一、背景
作为久战一线的牛马近日在看一个rcp 库的源码时,如下:
template
static typename std::enable_if, Args...)>::type>::value>::type
call_member(const F &f, Self *self, std::weak_ptr ptr,
std::string &result, std::tuple tp) {
auto r = call_member_helper(
f, self, typename nonstd::make_index_sequence{},
std::move(tp), ptr);
result = msgpack_codec::pack_args_str(result_code::OK, r);
}
虽说已久经沙场,但看到这几行代码依然感到头皮发麻,完全解释不了typename 在各处的意思,汗颜呐。作为一个有追求的一线牛马,必须搞明白它。上述代码提到的std:make_index_sequence 请看另一篇文章:
https://www.toutiao.com/article/7444487868501819955/
二、typename 是啥?
在C++模板元编程中,typename 是一个非常重要的关键字。typename 的主要用途是告诉编译器,接下来的符号应被解析为一个类型名称。
template
void print_type_size() {
std::cout << "Size of type: " << sizeof(T) << std::endl;
}
在这段代码中,typename 用于声明模板参数 T 的类型。在C++中,我们常说 “the type of T”(T的类型),这里的 “type”(类型)是指T可以代表的那些可能的类型,如int、std::vector等。也许你会想到上面这段代码中的typename换成class也一样可以,不错!那么这里便有了疑问,
- 这两种方式有区别么?
- 查看C++ Primer之后,发现两者完全一样。那么为什么C++要同时支持这两种方式呢?
- 既然class很早就已经有了,为什么还要引入typename这一关键字呢?
为了区别typename 与 class 在模板编程中的区别我们先来看一个例子:
template
void foo() {
T::iterator * iter;// ...
}
这段代码的目的是什么?多数人第一反应可能是和作者想的一样:定义一个指针iter,它指向的类型是包含在类作用域T中的iterator。T::iterator实际上可以是以下三种中的任何一种类型:
- 静态数据成员
- 静态成员函数
- 嵌套类型
前面例子中的ContainsAType::iterator是嵌套类型,完全没有问题。
嵌套类型:
struct ContainsAType {
struct iterator { /*...*/ };// ...
};
可如果是静态数据成员呢?如果实例化foo模板函数的类型是像这样的:
struct ContainsAnotherType {
static int iterator;// ...
};
然后如此实例化foo的类型参数:
foo();
那么,T::iterator * iter;被编译器实例化为
ContainsAnotherType::iterator * iter;,这是什么?前面是一个静态成员变量而不是类型,那么这便成了一个乘法表达式,只不过iter在这里没有定义,编译器会报错:
但如果iter是一个全局变量,那么这行代码将完全正确,它是表示计算两数相乘的表达式,返回值被抛弃。
同一行代码能以两种完全不同的方式解释,而且在模板实例化之前,完全没有办法来区分它们,这绝对是滋生各种bug的温床。这时C++标准委员会再也忍不住了,与其到实例化时才能知道到底选择哪种方式来解释以上代码,委员会决定引入一个新的关键字,这就是typename。先不着急下论断,将上述代码作如下修改:
template
void foo() {
typename T::iterator* iter;
}
struct ContainsAType {
struct iterator {
};
};
编译器就不报错了。为啥呢?先来看看C++ 标准委员会给typename 的定义:
A name used in a template declaration or definition and that is dependent on a template-parameter is assumed not to name a type unless the applicable name lookup finds a type name or the name is qualified by the keyword typename.
对于用于模板定义的依赖于模板参数的名称,只有在实例化的参数中存在这个类型名,或者这个名称前使用了typename关键字来修饰,编译器才会将该名称当成是类型。除了以上这两种情况,绝不会被当成是类型。因此,如果你想直接告诉编译器T::iterator是类型而不是变量,只需用typename修饰。这样编译器就可以确定T::iterator是一个类型,而不再需要等到实例化时期才能确定,因此消除了前面提到的歧义。
实际上,这两个关键字在模板参数声明中的语义是一样的。这个设计主要是为了保持对早期C++版本的兼容性,在早期版本中,只有class关键字可以用在这个位置。
既然typename关键字已经存在,而且它也可以用于最常见的指定模板参数,那么为什么不废除class这一用法呢?答案其实也很明显,因为在最终的标准出来之前,所有已存在的书、文章、教学、代码中都是使用的是class,可以想像,如果标准不再支持class,会出现什么情况。
对于指定模板参数这一用法,虽然class和typename都支持,但就个人而言我还是倾向使用typename多一些,因为我始终过不了class表示用户定义类型这道坎。另外,从语义上来说,typename比class表达的更为清楚。C++ Primer也建议使用typename:
使用关键字typename代替关键字class指定模板类型形参也许更为直观,毕竟,可以使用内置类型(非类类型)作为实际的类型形参,而且,typename更清楚地指明后面的名字是一个类型名。但是,关键字typename是作为标准C++的组成部分加入到C++中的,因此旧的程序更有可能只用关键字class。
2.1 typename 与 typedef 区别
- typedef:在C++中,我们经常使用typedef来创建一个已存在类型的别名(alias):
- typename:在模板编程中,typename被用来声明模板参数是一个类型:
下面是一个综合代码示例,它展示了typename和typedef在模板编程中的应用:
template
struct MyContainer {
typedef T* iterator; // 使用 typedef 定义了一个迭代器类型别名
void func() {
typename T::NestedType t; // 使用 typename 引用 T 的嵌套类型
// ...
}
};
在这个代码中,我们创建了一个名为MyContainer的模板,它定义了一个类型别名iterator(通过typedef),并在成员函数func中使用了typename来引用模板参数T的嵌套类型NestedType。
结论:在英语中,当我们描述这个过程时,我们会说“The typedef keyword is used to create an alias for the type `T*, and the typenamekeyword is used to tell the compiler thatT::NestedType is a type.”(typedef关键字被用来为T*创建一个别名,typename关键字被用来告诉编译器T::NestedType`是一个类型。)
小结:
关键字 | 描述 | 用途 | 示例 |
typename | 声明模板参数是一个类型 | 模板编程中引用模板类型参数的嵌套类型 | typename T::NestedType t; |
typedef | 创建一个已存在类型的别名 | 简化复杂的类型声明 | typedef int Integer; |
三、用法
3.1 typename 在常规场景中使用
3.1.1 在函数参数中使用typename
当我们想要在函数参数中使用依赖类型时,我们需要typename。在这种情况下,typename用于明确指定该类型是从模板参数中派生出来的类型。例如:
template
void function(typename T::SubType param);
在这个例子中,typename用于指定T::SubType是一个类型。
3.1.2 在函数返回值中使用typename
和函数参数一样,我们也可以在函数的返回值类型中使用typename,用于明确地指出该类型是从模板参数中派生出来的。例如:
template
typename T::SubType function();
在这个例子中,typename用于指定T::SubType是函数的返回类型。
3.1.3 在类成员中使用typename
typename也可以在类模板中使用,用于指定成员的类型。例如:
template
class MyClass {
typename T::SubType member;
};
在这个例子中,typename用于指定T::SubType是类成员member的类型。
3.1.4 小结
下面是一个对于这四种使用 typename 的方式的对比表:
使用场合 | 示例代码 | 描述 |
模板参数列表 | template | typename 在此处定义了 T 作为一个类型模板参数。 |
函数参数 | template | typename 在此处明确指定 T::SubType 是从模板参数 T 中派生的类型。 |
函数返回值 | template | typename 在此处明确指定 T::SubType 是从模板参数 T 中派生的类型,并且是函数的返回类型。 |
类成员 | template | typename 在此处明确指定 T::SubType 是从模板参数 T 中派生的类型,并且是类 MyClass 的成员的类型。 |
3.2. typename的其他使用场景
3.2.1在模板参数列表中使用typename
C++ 中的 typename 关键字在模板编程中有广泛的应用。让我们通过以下小节深入探讨其在不同场景下的用法。
在模板参数列表中,typename关键字用于表示类型模板参数。例如:
template
void func(T param) {
// ...
}
在这个示例中,T是一个类型模板参数。我们使用typename关键字来标记它。这样,当我们使用这个模板时,就可以将T替换为任意类型。例如:
func(10); // T被替换为int
func(20.0); // T被替换为double
在英语口语交流中,我们会这样叙述上述代码:“我们定义了一个模板函数func,其接收一个模板类型参数T。然后,我们通过调用func(10)和func(20.0),分别将T替换为int和double。”(We defined a template function func that takes a template type parameter T. Then, we instantiated func with T being int and double respectively by calling func(10) and func(20.0).)
3.3 在嵌套类型中使用typename
在模板编程中,我们经常需要处理嵌套类型(Nested Types)。这些类型通常是模板类中定义的类型,或者是模板参数类型的成员类型。在这种情况下,我们需要使用 typename 关键字来显式地告诉编译器,我们正在处理一个类型而不是一个值。例如:
template
void func() {
typename T::NestedType object;
// ...
}
在上述代码中,T::NestedType 是一个嵌套类型,我们使用 typename 关键字来表明这一点。这样,当我们使用模板时,就可以将 T::NestedType 替换为任意嵌套类型。
在英语口语交流中,我们会这样叙述上述代码:“我们定义了一个模板函数 func,其使用 typename 关键字来声明一个嵌套类型 T::NestedType。然后,我们可以在函数体内使用这个嵌套类型。”(We defined a template function func which uses typename keyword to declare a nested type T::NestedType. Then, we can use this nested type inside the function body.)
3.4 typename 在模板特化和偏特化中的应用
模板特化和偏特化是 C++ 模板编程中常见的技巧。当我们需要对某些特定类型提供特殊的处理时,我们可以使用模板特化或偏特化。在这些场景中,我们可能需要使用 typename 关键字。例如,假设我们有一个模板类 MyClass,它有一个嵌套类型 NestedType:
template
class MyClass {
public:
typedef T NestedType;
};
我们可以对 MyClass 进行特化,特化时我们需要用到 typename 关键字:
template <>
class MyClass {
public:
typedef int NestedType;
};
在这个特化版本的 MyClass 中,NestedType 总是为 int 类型。我们使用 typename 关键字来指定 NestedType 的类型。
在下一节中,我们将探讨 typename 在 std::enable_if 中的应用,这是一个非常实用的技巧,可以让我们的模板代码更加灵活和强大。
3.3 typename 在 std::enable_if 中的应用
std::enable_if 是一个常用于模板元编程的工具,它允许我们根据条件启用或禁用模板。在 std::enable_if 中,我们通常需要使用 typename 关键字。例如,假设我们有一个函数模板 func,我们想要只在 T 是整数类型时才使其可用,我们可以这样做:
template
typename std::enable_if::value>::type
func(T param) {
// ...
}
在这个示例中,我们使用了 std::is_integral::value 来检查 T 是否为整数类型。如果是,则 std::enable_if 将定义一个名为 type 的类型,否则 std::enable_if 不定义任何类型。由于 typename 关键字的存在,当 T 不是整数类型时,编译器将在编译时期就检测到这个错误,而不会产生错误的运行时行为。现在就好解释文章开头处那个std:enable_if 前的typename 的含义了:
template
static typename std::enable_if, Args...)>::type>::value>::type
call_member(const F &f, Self *self, std::weak_ptr ptr,
std::string &result, std::tuple tp) {
auto r = call_member_helper(
f, self, typename nonstd::make_index_sequence{},
std::move(tp), ptr);
result = msgpack_codec::pack_args_str(result_code::OK, r);
}
3.3 typename的高级用法
在这个部分,我们将深入探讨typename的高级用法,包括多层typename语句,多条typename语句,以及typename与std::enable_if的组合应用。
3.3.1 多层typename语句
有时,我们需要使用多层嵌套的模板类型。在这种情况下,我们可能需要使用多层typename语句。让我们通过一个例子来看看如何使用多层typename语句:
template
class Outer {
public:
template
class Inner {};
};
template
void func(typename Outer::template Inner param) {
// 实现细节...
}
在这个例子中,func函数接收一个类型为Outer::Inner的参数。由于Outer::Inner是一个依赖于模板参数的类型,因此我们需要在前面加上typename关键字。同时,由于Inner是Outer的一个模板成员,所以我们需要在Inner前面加上template关键字。
这样的语法在一开始可能会让人觉得有些困惑,但只要理解了它的原理,就会发现其实并不复杂。关键在于理解这样一个事实:在C++中,模板是一种生成类型的机制。因此,我们需要使用typename和template关键字来告诉编译器,我们正在处理的是类型,而不是值。
3.3.2 多条typename语句
在某些情况下,我们可能需要在一条语句中使用多个typename。例如,我们可能需要在一条函数声明中指定多个模板参数类型。下面是一个例子:
template
class MyClass {
public:
typedef T type1;
typedef U type2;
};
template
void func(typename MyClass::type1 param1, typename MyClass::type2 param2) {
// 实现细节...
}
在这个例子中,func函数接收两个参数,分别是MyClass::type1和MyClass::type2类型。由于这两种类型都依赖于模板参数,所以我们需要在每个类型前面都加上typename关键字。
3.3.3 typename与std::enable_if的组合应用
我们可以将typename与std::enable_if结合起来使用,以实现更复杂的模板特化和条件编
译。std::enable_if是一个模板,它可以根据模板参数的值来决定其是否有type成员类型。下面是一个例子:
template
typename std::enable_if::value, T>::type
process(T t) {
std::cout << t << " is an integral number." << std::endl;
}
在这个例子中,process函数只能处理整数类型的参数。当我们试图使用一个非整数类型的参数调用process函数时,会导致编译错误。这是因为在std::enable_if的条件为false时,它没有type成员类型,因此typename std::enable_if::value, T>::type这个类型就不存在,导致process函数无法被声明。
以上就是typename的高级用法。尽管这些用法在一开始可能会让人觉得有些困惑,但只要理解了其原理,就会发现它们其实非常强大,能够帮助我们编写更灵活、更强大的模板代码。
3.3.4 typename的应用:同时使用模板参数列表、函数参数列表和返回值类型进行类型检查
在模板元编程中,我们常常需要进行一些复杂的类型检查。在这个过程中,std::enable_if和typename两者的组合使用提供了一个强大的工具。具体来说,我们可以在模板参数列表、函数参数列表和函数返回值类型中同时使用它们,以进行多个条件的检查。
以下是一个示例,它演示了如何在一个函数中同时使用这三种方式进行类型检查:
// 模板函数,同时使用模板参数列表、函数参数列表和函数返回值类型进行类型检查
template<
typename T, // 模板参数 T
// 在模板参数列表中使用 typename 进行检查,T 必须是整型
typename std::enable_if::value>::type * = nullptr
// 在返回值类型中使用 typename 进行检查,T 的大小必须大于 4 字节
typename std::enable_if<(sizeof(T) > 4), T>::type
process(
T value, // 函数参数 value
// 在函数参数列表中使用 typename 进行检查,T 必须是有符号类型
typename std::enable_if::value, T>::type * = nullptr
) {
std::cout << value << " is an integral number, its size is greater than 4 bytes, and it is signed." <<
std::endl;
return
value;
}
// 模板类,使用模板参数列表进行类型检查,T 必须是整型
template<
typename T, // 模板参数 T
typename std::enable_if::value>::type * = nullptr
class MyClass {
} ;
int main() {
long l = 42;
process(l); // 此处可以成功编译和运行,因为 long 是整型,大于 4 字节,且是有符号类型
// 下面的代码将无法编译,因为 unsigned long 不是有符号类型
// unsigned long ul = 42;
// process(ul);
MyClass myClass; // 成功实例化 MyClass,因为 int 是整型
return 0;
}
通过这种方式,我们可以在编译期间进行非常细粒度的类型检查,从而保证我们的代码在运行期间能够按照预期的方式工作。
四. typename语句的限制及常见问题
在深入探索C++的模板元编程中,我们已经了解到typename语句是一个强大且灵活的工具。然而,尽管它有很多优点,但在使用过程中也需要注意一些限制和常见问题。
4.1 不能用于基本数据类型
C++ 的模板元编程规定,typename语句无法应用于基本数据类型。换言之,尝试在int或double这样的基本数据类型之前使用typename语句会导致编译错误。例如:
template
void function(typename T::subtype var) { // Do something with var. }
在这种情况下,如果你尝试将T替换为int或double(这是没有subtype的基本数据类型),那么编译器就会报错。
4.2 不能用于非类型模板参数
在模板元编程中,有类型和非类型两种模板参数,而typename只能用于类型模板参数。换言之,如果模板参数是非类型的(例如整数,指针,引用等),则不能使用typename关键字。
这是由于typename关键字的设计初衷是为了解决在模板中表示嵌套依赖类型的问题。非类型模板参数不具有嵌套类型,因此在这种情况下使用typename关键字是不合适的。
例如:
template
class MyClass {
T data[N];
};
在这个例子中,typename可以用于T,因为T是一个类型模板参数。但是对于N,我们不能使用typename,因为N是一个非类型模板参数。
总的来说,理解和正确使用typename关键字是掌握C++模板元编程的重要步骤。希望通过这个章节,你能够更深入的理解typename语句的一些限制及常见问题。
4.3 小结
最后这个规则看起来有些复杂,可以参考MSDN:
- typename在下面情况下禁止使用:
- 模板定义之外,即typename只能用于模板的定义中
- 非限定类型,比如前面介绍过的int,vector
之类 - 基类列表中,比如template
class C1 : T::InnerType 不能在T::InnerType前面加typename - 构造函数的初始化列表中
- 如果类型是依赖于模板参数的限定名,那么在它之前必须加typename(除非是基类列表,或者在类的初始化成员列表中)
- 其它情况下typename是可选的,也就是说对于一个不是依赖名的限定名,该名称是可选的,例如vector
vi;
五、参考文档:
https://developer.aliyun.com/article/1465436
https://feihu.me/blog/2014/the-origin-and-usage-of-typename/