转载:https://blog.csdn.net/vanturman/article/details/80269081

近日,看到这样一行代码:

typedef typename __type_traits<T>::has_trivial_destructor trivial_destructor;

看起来它应该是定义一个类型别名,但是typedef不应该是像这样使用么,typedef+原类型名+新类型名:

typedef char* PCHAR;

可为何此处多了一个typename?另外__type_traits又是什么?

看起来有些眼熟,想起之前在Effective C++上曾经看过traits这一技术的介绍,和这里的__type_traits有点像。只是一直未曾遇到需要traits的时候,所以当时并未仔细研究。然而STL中大量的充斥着各种各样traits,一查才发现原来它是一种非常高级的技术,在更现的高级语言中已经很普遍。因此这次花了些时间去学习它,接下来还有会有另一篇文章来详细介绍C++的traits技术。在这里,我们暂时忘记它,仅将它当成一个普通的类,先来探讨一下这个多出来的typename是怎么回事?

typename的常见用法

对于typename这个关键字,如果你熟悉C++的模板,一定会知道它有这样一种最常见的用法(代码摘自C++ Primer)

// implement strcmp-like generic compare function

// returns 0 if the values are equal, 1 if v1 is larger, -1 if v1 is smaller

template <typename T>

int compare(const T &v1, const T &v2)

{

    if (v1 < v2) return -;

    if (v2 < v1) return ;

    return ;

}

也许你会想到上面这段代码中的typename换成class也一样可以,不错!那么这里便有了疑问,这两种方式有区别么?查看C++ Primer之后,发现两者完全一样。那么为什么C++要同时支持这两种方式呢?既然class很早就已经有了,为什么还要引入typename这一关键字呢?问的好,这里面有一段鲜为人知的历史(也许只是我不知道:-))。带着这些疑问,我们开始探寻之旅。

typename的来源

对于一些更早接触C++的朋友,你可能知道,在C++标准还未统一时,很多旧的编译器只支持class,因为那时C++并没有typename关键字。记得我在学习C++时就曾在某本C++书籍上看过类似的注意事项,告诉我们如果使用typename时编译器报错的话,那么换成class即可。

一切归结于历史。

Stroustrup在最初起草模板规范时,他曾考虑到为模板的类型参数引入一个新的关键字,但是这样做很可能会破坏已经写好的很多程序(因为class已经使用了很长一段时间)。但是更重要的原因是,在当时看来,class已完全足够胜任模板的这一需求,因此,为了避免引起不必要的麻烦,他选择了妥协,重用已有的class关键字。所以只到ISO C++标准出来之前,想要指定模板的类型参数只有一种方法,那便是使用class。这也解释了为什么很多旧的编译器只支持class

但是对很多人来说,总是不习惯class,因为从其本来存在的目的来说,是为了区别于语言的内置类型,用于声明一个用户自定义类型。那么对于下面这个模板函数的定义(相对于上例,仅将typename换成了class

template <class T>

int compare(const T &v1, const T &v2)
{
if (v1 < v2) return -; if (v2 < v1) return ; return ;
}

从表面上看起来就好像这个模板的参数应该只支持用户自定义类型,所以使用语言内置类型或者指针来调用该模板函数时总会觉得有一丝奇怪(虽然并没有错误):

  1. int v1 = , v2 = ;
    
    int ret = compare(v1, v2);
    
    int *pv1 = NULL, *pv2 = NULL;
    
    ret = compare(pv1, pv2);

令人感到奇怪的原因是,class在类和模板中表现的意义看起来存在一些不一致,前者针对用户自定义类型,而后者包含了语言内置类型和指针。也正因为如此,人们似乎觉得当时没有引入一个新的关键字可能是一个错误。

这是促使标准委员会引入新关键字的一个因素,但其实还有另外一个更加重要的原因,和文章最开始那行代码相关。

一些关键概念

在我们揭开真实原因的面纱之前,先保持一点神秘感,因为为了更好的理解C++标准,有几个重要的概念需要先行介绍一下。

限定名和非限定名

限定名(qualified name),故名思义,是限定了命名空间的名称。看下面这段代码,coutendl就是限定名:

#include <iostream>

int main() {

std::cout << "Hello world!" << std::endl;

}

coutendl前面都有std::,它限定了std这个命名空间,因此称其为限定名。

如果在上面这段代码中,前面用using std::cout;或者using namespace std;,然后使用时只用coutendl,它们的前面不再有空间限定std::,所以此时的coutendl就叫做非限定名(unqualified name)。

依赖名和非依赖名

依赖名(dependent name)是指依赖于模板参数的名称,而非依赖名(non-dependent name)则相反,指不依赖于模板参数的名称。看下面这段代码:

template <class T>

class MyClass
{ int i; vector<int> vi; vector<int>::iterator vitr; T t; vector<T> vt; vector<T>::iterator viter; };

因为是内置类型,所以类中前三个定义的类型在声明这个模板类时就已知。然而对于接下来的三行定义,只有在模板实例化时才能知道它们的类型,因为它们都依赖于模板参数T。因此,Tvector<T>vector<T>::iterator称为依赖名。前三个定义叫做非依赖名。

更为复杂一点,如果用了typedef T U; U u;,虽然T没再出现,但是U仍然是依赖名。由此可见,不管是直接还是间接,只要依赖于模板参数,该名称就是依赖名。

类作用域

在类外部访问类中的名称时,可以使用类作用域操作符,形如MyClass::name的调用通常存在三种:静态数据成员、静态成员函数和嵌套类型:

struct MyClass
{ static int A; static int B(); typedef int C; }

MyClass::AMyClass::BMyClass::C分别对应着上面三种。

引入typename的真实原因

结束以上三个概念的讨论,让我们接着揭开typename的神秘面纱。

一个例子

在Stroustrup起草了最初的模板规范之后,人们更加无忧无虑的使用了class很长一段时间。可是,随着标准化C++工作的到来,人们发现了模板这样一种定义:

template <class T>

void foo() {

T::iterator * iter;

// ...

}

这段代码的目的是什么?多数人第一反应可能是:作者想定义一个指针iter,它指向的类型是包含在类作用域T中的iterator。可能存在这样一个包含iterator类型的结构:

struct ContainsAType {

struct iterator { /*...*/ };

// ...

};

然后像这样实例化foo

foo<ContainsAType>();

这样一来,iter那行代码就很明显了,它是一个ContainsAType::iterator类型的指针。到目前为止,咱们猜测的一点不错,一切都看起来很美好。

问题浮现

在类作用域一节中,我们介绍了三种名称,由于MyClass已经是一个完整的定义,因此编译期它的类型就可以确定下来,也就是说MyClass::A这些名称对于编译器来说也是已知的。

可是,如果是像T::iterator这样呢?T是模板中的类型参数,它只有等到模板实例化时才会知道是哪种类型,更不用说内部的iterator。通过前面类作用域一节的介绍,我们可以知道,T::iterator实际上可以是以下三种中的任何一种类型:

  • 静态数据成员
  • 静态成员函数
  • 嵌套类型

前面例子中的ContainsAType::iterator是嵌套类型,完全没有问题。可如果是静态数据成员呢?如果实例化foo模板函数的类型是像这样的:

struct ContainsAnotherType {

static int iterator;

// ...

};

然后如此实例化foo的类型参数:

foo<ContainsAnotherType>();

那么,T::iterator * iter;被编译器实例化为ContainsAnotherType::iterator * iter;,这是什么?前面是一个静态成员变量而不是类型,那么这便成了一个乘法表达式,只不过iter在这里没有定义,编译器会报错:

error C2065: ‘iter’ : undeclared identifier

但如果iter是一个全局变量,那么这行代码将完全正确,它是表示计算两数相乘的表达式,返回值被抛弃。

同一行代码能以两种完全不同的方式解释,而且在模板实例化之前,完全没有办法来区分它们,这绝对是滋生各种bug的温床。这时C++标准委员会再也忍不住了,与其到实例化时才能知道到底选择哪种方式来解释以上代码,委员会决定引入一个新的关键字,这就是typename

千呼万唤始出来

我们来看看C++标准

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修饰:

template <class T>

void foo() {

typename T::iterator * iter;

// ...

}

这样编译器就可以确定T::iterator是一个类型,而不再需要等到实例化时期才能确定,因此消除了前面提到的歧义。

不同编译器对错误情况的处理

但是如果仍然用ContainsAnotherType来实例化foo,前者只有一个叫iterator的静态成员变量,而后者需要的是一个类型,结果会怎样?我在Visual C++ 2010和g++ 4.3.4上分别做了实验,结果如下:

Visual C++ 2010仍然报告了和前面一样的错误:

error C2065: ‘iter’ : undeclared identifier

虽然我们已经用关键字typename告诉了编译器iterator应该是一个类型,但是用一个定义了iterator变量的结构来实例化模板时,编译器却选择忽略了此关键字。出现错误只是由于iter没有定义。

再来看看g++如何处理这种情况,它的错误信息如下:

In function ‘void foo() [with T = ContainsAnotherType]’:instantiated from hereerror: no type named ‘iterator’ in ‘struct ContainsAnotherType’

g++在ContainsAnotherType中没有找到iterator类型,所以直接报错。它并没有尝试以另外一种方式来解释,由此可见,在这点上,g++更加严格,更遵循C++标准。

使用typename的规则

最后这个规则看起来有些复杂,可以参考MSDN

  • typename在下面情况下禁止使用:

    • 模板定义之外,即typename只能用于模板的定义中
    • 非限定类型,比如前面介绍过的intvector<int>之类
    • 基类列表中,比如template <class T> class C1 : T::InnerType不能在T::InnerType前面加typename
    • 构造函数的初始化列表中
  • 如果类型是依赖于模板参数的限定名,那么在它之前必须加typename(除非是基类列表,或者在类的初始化成员列表中)
  • 其它情况下typename是可选的,也就是说对于一个不是依赖名的限定名,该名称是可选的,例如vector<int> vi;

其它例子

对于不会引起歧义的情况,仍然需要在前面加typename,比如:

template <class T>

void foo() {

typename T::iterator iter;

// ...

}

不像前面的T::iterator * iter可能会被当成乘法表达式,这里不会引起歧义,但仍需加typename修饰。

再看下面这种:

template <class T>

void foo() {

typedef typename T::iterator iterator_type;

// ...

}

是否和文章刚开始的那行令人头皮发麻的代码有些许相似?没错!现在终于可以解开typename之迷了,看到这里,我相信你也一定可以解释那行代码了,我们再看一眼:

typedef typename __type_traits<T>::has_trivial_destructor trivial_destructor;

它是将__type_traits<T>这个模板类中的has_trivial_destructor嵌套类型定义一个叫做trivial_destructor的别名,清晰明了。

再看常见用法

既然typename关键字已经存在,而且它也可以用于最常见的指定模板参数,那么为什么不废除class这一用法呢?答案其实也很明显,因为在最终的标准出来之前,所有已存在的书、文章、教学、代码中都是使用的是class,可以想像,如果标准不再支持class,会出现什么情况。

对于指定模板参数这一用法,虽然classtypename都支持,但就个人而言我还是倾向使用typename多一些,因为我始终过不了class表示用户定义类型这道坎。另外,从语义上来说,typenameclass表达的更为清楚。C++ Primer也建议使用typename:

使用关键字typename代替关键字class指定模板类型形参也许更为直观,毕竟,可以使用内置类型(非类类型)作为实际的类型形参,而且,typename更清楚地指明后面的名字是一个类型名。但是,关键字typename是作为标准C++的组成部分加入到C++中的,因此旧的程序更有可能只用关键字class。

参考

  1. C++ Primer
  2. Effective C++
  3. A Description of the C++ typename keyword
  4. 维基百科typename
  5. 另外关于typename的历史,Stan Lippman写过一篇文章,Stan Lippman何许人,也许你不知道他的名字,但看完这些你一定会发出,“哦,原来是他!”:他是 C++ Primer, Inside the C++ Object Model, Essential C++, C# Primer 等著作的作者,另外他也曾是Visual C++的架构师。
  6. StackOverflow上有一个非常深入的回答,感谢@Emer 在本文评论中提供此链接。

写在结尾

一个简单的关键字就已经充满曲折,这可以从一个角度反映出一门语言的发展历程,究竟要经历多少决断、波折与妥协,最终才发展成为现在的模样。在一个特定的时期,由于历史、技术、思想等各方面的因素,设计总会向现实做出一定的让步,出现一些“不完美”的设计,为了保持向后兼容,有些“不完美”的历史因素被保留了下来。现在我可以理解经常为人所诟病的Windows操作系统,Intel芯片,IE浏览器,Visual C++等,为了保持向后兼容,不得不在新的设计中仍然保留这些“不完美”,虽然带来的是更多的优秀特性,但有些人却总因为这些历史因素而唾弃它们,也为自己曾有一样的举动而羞愧不已。但也正是这些“不完美”的出现,才让人们在后续的设计中更加注意,站在前人的肩膀上,做出更好,更完善的设计,于是科技才不断向前推进。

然而也有一些敢于大胆尝试的例子,比如C++ 11,它的变化之大甚至连Stroustrup都说它像一门新语言。对于有着30余年历史的“老”语言,不仅没有被各种新贵击溃,反而在不断向晚辈们借鉴,吸纳一些好的特性,老而弥坚,这十分不易。还有Python 3,为了清理2.x版本中某些语法方面的问题,打破了与2.x版本的向后兼容性,这种牺牲向后兼容换取进步的做法固然延缓了新版本的接受时间,但我相信这是向前进步的阵痛。Guido van Rossum的这种破旧立新的魄力实在让人钦佩,至于这种做法能否最终为人们所接受,一切交给历史来检验。

c++类模板template中的typename使用方法-超级棒的更多相关文章

  1. C++ - 模板(template)中typename的使用方法

    声明template参数时, 前缀关键字class和typename可以互换; 使用关键字typename标识嵌套从属类型名称, 但不需在基类列表和成员初始化列表内使用. 从属名称(dependent ...

  2. 类模板 template<class T>

    参考网址:http://c.biancheng.net/cpp/biancheng/view/213.html // demo3.cpp : 定义控制台应用程序的入口点. // #include &q ...

  3. C++类模板 template <class T>

    C++在发展的后期增加了模板(template )的功能,提供了解决这类问题的途径.可以声明一个通用的类模板,它可以有一个或多个虚拟的类型参数. 比如: class Compare_int class ...

  4. 智能指针类模板(中)——Qt中的智能指针

    Qt中的智能指针-QPointer .当其指向的对象被销毁时,它会被自动置空 .析构时不会自动销毁所指向的对象-QSharedPointer .引用计数型智能指针 .可以被自由的拷贝和赋值 .当引用计 ...

  5. vue 在模板template中变量和字符串拼接

    例子:  :post-action="'/api/v1/reportPage/'+this.selectedPagerId+'/saveimg/'"

  6. 【Template】template中如果包含post方法的form, 要在<form>之后添加{% csrf_token %}标签

    template模板标签{% csrf_token %} 和CSRF middleware提供了易于使用的防“跨站点伪造攻击”的保护, 详情请阅读官方文档https://docs.djangoproj ...

  7. ES6中数组新增的方法-超级好用

    Array.find((item,indexArr,arr)=>{}) 掌握 找出第一个符合条件的数组成员. 它的参数是一个回调函数,对所有数组成员依次执行该回调函数. 直到找出第一个返回值为t ...

  8. C++:类模板与模板类

    6.3 类模板和模板类 所谓类模板,实际上是建立一个通用类,其数据成员.成员函数的返回值类型和形参类型不具体指定,用一个虚拟的类型来代表.使用类模板定义对象时,系统会实参的类型来取代类模板中虚拟类型从 ...

  9. c++的函数模板和类模板

    函数模板和普通函数区别结论: 函数模板不允许自动类型转化 普通函数能够进行自动类型转换 函数模板和普通函数在一起,调用规则: 1 函数模板可以像普通函数一样被重载 2 C++编译器优先考虑普通函数 3 ...

随机推荐

  1. AtomicReference 和 volatile 的区别

    顾名思义,就是不会被打断!!!!!! https://www.cnblogs.com/lpthread/p/3909231.html java.util.concurrent.atomic工具包,支持 ...

  2. 运行ceph时,了解一下主要的进程。

    最简单ceph.conf配置如下: [global] fsid = 798ed076--429e-9e27-0ffccd60b56e mon_initial_members = ceph-node1 ...

  3. CentOS7.5安装网易云音乐

    CentOS7中一直没有一个像样的音乐播放器,网易云音乐与深度科技团队在半年前就启动了“网易云音乐Linux版“, 但是只提供了Ubuntu(14.04&16.04)和deepin15版本,并 ...

  4. Java中多线程问题

    线程调度中的方法: sleep() 顾名思义线程休眠可传递连个参数-@毫秒 @纳秒 yield() 暂时挂起 这里的线程会释放资源,但是有一个坑是虽然是释放资源但是是公平竞争资源 如:a线程释放资源后 ...

  5. Win7 + VirtualBox + CentOS(无桌面), 扩容

    http://www.2cto.com/os/201401/269730.html 对于目前的网络开发者来说,比较好的搭档就是Win7+VirtualBox+CentOS的组合,既可以发挥Linux强 ...

  6. vim的保存文件和退出命令

    文章来源:http://blog.sina.com.cn/s/blog_5e357d2d0100zmth.html 命令 简单说明 :w 保存编辑后的文件内容,但不退出vim编辑器.这个命令的作用是把 ...

  7. HDU 6186 CS Course【前后缀位运算枚举/线段树】

    [前后缀枚举] #include<cstdio> #include<string> #include<cstdlib> #include<cmath> ...

  8. Bzoj 3498 Cakes(三元环)

    题面(权限题就不放题面了) 题解 三元环模板题,按题意模拟即可. #include <cstdio> #include <cstring> #include <vecto ...

  9. Flask实战第46天:完成前台登录功能

    后台逻辑 首先进行表单验证, 编辑front.froms.py ... class SignInForm(BaseForm): telephone = StringField(validators=[ ...

  10. [xsy1144]选物品

    题意:给定$a_{1\cdots n},b_{1\cdots n}$,询问是给定$l,r$,找出$a',b'$使得$\sum\limits_{i=l}^r\max(\left|a'-a_i\right ...