读书笔记 effective c++ Item 42 理解typename的两种涵义
1. class和typename含义相同的例子
问题:在下面的模板声明中class和typename的区别是什么?
1 template<class T> class Widget; // uses “class” 2 3 template<typename T> class Widget; // uses “typename”
答案:没有任何区别。当声明一个模板类型参数时,class和typename意味着相同的事情。一些程序员喜欢使用class,因为容易敲打。其他的(包括我)更加喜欢使用typename,因为用它表明参数不需要是一个class类型。一些程序员在允许使用任何type的时候使用typename,只用对用户自定义的类型使用class。但是从C++ 的观点来看,在声明模板参数的时候class和typename意味着相同的事情。
2. 必须使用typename的例子
然而,C++并不总是将class和typename同等对待。有时你必须使用typename。为了理解在什么时候必须使用,我们必须讨论能够在模板中引用的两种名字。
假设我们有一个函数模板,用和STL兼容的容器作为模板参数,此容器中包含的对象能够被赋值给int类型。进一步假设这个函数打印容器中的第二个元素值。我在下面以愚蠢的方式实现了一个愚蠢的函数,它甚至不能通过编译,但是请忽略这些事情,看下面的例子:
1 template<typename C> // print 2nd element in 2 void print2nd(const C& container) // container; 3 { // this is not valid C++! 4 if (container.size() >= 2) { 5 C::const_iterator iter(container.begin()); // get iterator to 1st element 6 ++iter; // move iter to 2nd element 7 int value = *iter; // copy that element to an int 8 9 std::cout << value; // print the int 10 11 } 12 13 }
我对此函数中的两个本地变量做了高亮,iter和value。Iter的类型是C::const_iterator,它依赖于模板参数C。模板中依赖于模板参数的名字被称作依赖名字(dependent names)。当一个依赖名字嵌套在一个类中的时候,我把它叫做内嵌依赖名字(nested dependent name)。C::const_iterator是一个内嵌依赖名字。事实上,它是一个内嵌依赖类型名字(nested dependent type name),也即是指向一个类型(type)的内嵌依赖名字。
对于print2nd中的其他本地变量,value,类型为int。int不依赖于任何模板参数。这种名字被称作“非依赖名字”(non-dependent names)。(我不知道为什么不把它们叫做独立名字(independent names)。“non-dependent”是一种不好的命名方式,但毕竟它是术语,所以需要遵守这个约定。)
内嵌依赖名字会导致解析困难。例如,如果我们让print2nd函数以下面的方式开始,会更加愚蠢:
1 template<typename C> 2 3 void print2nd(const C& container) 4 5 { 6 7 C::const_iterator * x; 8 9 ... 10 11 }
看上去像是我们声明了一个本地变量x,这个x指针指向一个C::const_iterator。但是它看上去是这样的仅仅因为我们“知道”C::const_iterator是一个type。但是如果C::const_iterator不是一个type会是怎样呢?如果C有个静态数据成员恰好被命名为const_iterator会发生什么?如果x恰巧是一个全局变量的名字呢?在这种情况下,上面的code就不会声明一个本地变量,它会是C::const_iterator和x的乘积!听起来有些疯狂,但这是可能的,实现C++编译器的人员也必须考虑到所有可能的输入,包括一些看起来很疯狂的例子。
直到C被确定之前,没有办法知道C::const_iterator是否是一个type,当函数模板print2nd被解析的时候,C不能够被确认。为了处理这种模棱两可的问题,C++有一个准则:如果解析器在模板中碰到了一个内嵌依赖名字,它不会认为这是一个type,除非你告诉它。默认情况下,内嵌依赖名字不是types。(对于这个规则有个例外,一会会提到。)
将上面的规则记在心中,再看一次print2nd的开始部分:
1 template<typename C> 2 void print2nd(const C& container) 3 { 4 if (container.size() >= 2) { 5 C::const_iterator iter(container.begin()); // this name is assumed to 6 ... // not be a type
现在应该清楚为什么这不是有效的C++了。Iter的声明只有在C::const_iterator是一个type的情况下才有意义,但是我们并没有告知C++它是一个类型,于是C++假设它不是一个类型。为了纠正这种情况,我们必须告诉C++ C::const_iterator是一个类型。我们将typename放在type之前就能达到这个目的:
1 template<typename C> // this is valid C++ 2 3 void print2nd(const C& container) 4 5 { 6 7 if (container.size() >= 2) { 8 9 typename C::const_iterator iter(container.begin()); 10 11 ... 12 13 } 14 15 }
这个规则很简单:在一个模板中,任何时候你引用一个内嵌依赖类型名字,你都必须在名字前加上typename。(也有例外,一会会提到。)
typename应该只被用来确认一个内嵌依赖类型名字;其他的名字不应该加这个前缀。例如,下面的函数模板使用两个参数,一个容器和一个容器的迭代器:
1 template<typename C> // typename allowed (as is “class”) 2 void f(const C& container, // typename not allowed 3 typename C::iterator iter); // typename required
C不是内嵌依赖类型名字(它没有内嵌在任何依赖于模板参数的东西中),所以在声明容器的时候不应该加typename,但是C::iterator是一个内嵌依赖类型名字,所以需要加typename。
3. 一个例外——不能使用typename的地方
”typename”必须加在内嵌依赖类型名字之前“这个规则有一个例外:基类列表中的内嵌依赖类型名字或者成员初始化列表中的基类标识符不能加typename。例如:
1 template<typename T> 2 class Derived: public Base<T>::Nested { // base class list: typename not 3 4 public: // allowed 5 6 explicit Derived(int x) 7 8 9 10 : Base<T>::Nested(x) // base class identifier in mem. 11 12 { // init. list: typename not allowed 13 14 15 typename Base<T>::Nested temp; // use of nested dependent type 16 ... // name not in a base class list or 17 } // as a base class identifier in a 18 ... // mem. init. list: typename 19 required 20 };
这种不一致性令人感到厌烦,但是一旦你有了一点经验,你就会注意到它。
4. 最后的例子——为typename使用typedef
让我们看最后一个typename的例子,因为它代表了你将会在真实代码中看到的某些东西。假设我们正在实现一个函数模板,带了一个迭代器参数,我们想为迭代器指向的对象做一份本地拷贝,temp。我们可以像下面这样实现:
1 template<typename IterT> 2 void workWithIterator(IterT iter) 3 { 4 typename std::iterator_traits<IterT>::value_type temp(*iter); 5 ... 6 }
不要让 std::iterator_traits<IterT>::value_type 吓到你。这只是标准特性类(standard traits class)的一种使用方法,这是“类型IterT对象指向的类型“的C++实现方式。这个句子声明了一个本地变量(temp),它的类型同IterT对象指向的对象的类型一致,它将temp初始化为iter指向的对象。如果IterT是vector<int>::iterator,那么temp就是int类型的。如果IterT是list<string>::iterator,temp就是string类型的。因为std::iterator_traits<IterT>::value_type是一个内嵌依赖类型名字(在iterator_traits<IterT>内部value_type是内嵌的,IterT是一个模板参数),我们必须为其添加typename。
如果你认为读std::iterator_traits<IterT>::value_type是一件不让人愉快的事情,想像一下将其打出来会是什么样的。如果你像大部分程序员一样,多次输入这个表达式的想法是可怕的,所以你会想为其创建一个typedef。对于像value_type这样的特性(traits)成员名字来说(对于特性的信息看Item47),使用惯例是使得typedef名字和特性成员名字相同,所以这样一个本地typedef通常被定义成下面这样:
1 template<typename IterT> 2 void workWithIterator(IterT iter) 3 { 4 typedef typename std::iterator_traits<IterT>::value_type value_type; 5 value_type temp(*iter); 6 ... 7 }
许多程序员发现将“typedef typename“并列看上去不和谐,但是对于使用内嵌依赖类型名字的规则来说,这是一个合乎逻辑的结果。你会很快习惯这种用法。毕竟,你有着很强的驱动力。你想输入typename std::iterator_traits<IterT>::value_type多少次呢?
5. Typename的执行因编译器而异
作为结束语,我应该提及的是关于typename规则的强制执行随着编译器的不同而不同,一些编译器接受需要typename但实际上没有输入的情况;一些编译器接受输入了typename但实际上不允许的情况;还有一些(通常是老的编译器)在需要输入typename时拒绝了typename输入。这就意味着typename和内嵌依赖类型名字的交互会产生让你头痛的问题。
6. 总结
- 当声明模板参数的时候,class和typename是可以互换的。
- 使用typename来识别内嵌依赖类型名字,但在基类列表中或者成员初始化列表中的基类标识符除外。
作者:
HarlanC
博客地址:
http://www.cnblogs.com/harlanc/
个人博客:
http://www.harlancn.me/
本文版权归作者和博客园共有,欢迎转载,但未经作者同意必须保留此段声明,且在文章页面明显位置给出,
原文链接