C++11标准发布已有一段时间了, 维基百科上有对C++11新标准的变化和C++11新特性介绍的文章。
以下是关于C++11的英文版本和中文版本维基百科的链接:
http://en.wikipedia.org/wiki/C++11
http://zh.wikipedia.org/wiki/C++0x
0.简介
C++11,之前被称作C++0x,即ISO/IEC 14882:2011,是目前的C++编程语言的正式标准。它取代第二版标准ISO/IEC 14882:2003(第一版ISO/IEC 14882:1998发布于1998年,第二版于2003年发布,分别通称C++98以及C++03,两者差异很小)。新的标准包含了几个核心语言增加的新特性,而且扩展C++标准程序库,并入了大部分的C++ Technical Report 1程序库(数学的特殊函数除外)。最新的消息被公布在 ISO C++ 委员会网站(英文)。
ISO/IEC JTC1/SC22/WG21 C++ 标准委员会计划在2010年8月之前完成对最终委员会草案的投票,以及于2011年3月召开的标准会议完成国际标准的最终草案。然而,WG21预期ISO将要花费六个月到一年的时间才能正式发布新的C++标准。为了能够如期完成,委员会决定致力于直至2006年为止的提案,忽略新的提案。最终,于2011年8月12日公布,并于2011年9月出版。2012年2月28日的国际标准草案(N3376)是最接近于现行标准的草案,差异仅有编辑上的修正。
像C++这样的编程语言,通过一种演化的过程来发展其定义。这个过程不可避免地将引发与现有代码的兼容问题。不过根据Bjarne Stroustrup(C++的创始人,标准委员会的一员)表示,新的标准将几乎100%兼容现有标准。
1.来自以往版本标准的变更(候选变更)
C++的修订范围包括核心语言以及标准程序库。在开发2011版标准的各个特性的过程中,标准委员会坚持以下指导思想:
* 维持与C++98,可能的话还有C之间的兼容性与稳定性;
* 尽可能通过通过标准程序库来引进新的特性, 而不是扩展核心语言;
* 能够促进编程技术的变更优先;
* 改进 C++ 以帮助系统和程序库的设计,而不是引进只对特定应用有用的新特性;
* 增强类型安全,给现行不安全的技术提供更安全的替代方案;
* 增强直接与硬件协同工作的性能和能力;
* 为现实世界中的问题提供适当的解决方案;
* 实行零负担原则(如果某些功能要求的额外支持,那么只有在该功能被用到时这些额外的支持才被用到);
* 使C++易于教学
注重对初学者的关注,因为他们构成了计算机程序员的主体。也因为许多初学者不愿扩展他们的C++知识,他们仅限于掌握C++中自己所专精的部分.
2.核心语言的扩充
C++标准委员会的一个职责是开发语言核心. 核心语言被大幅改进的领域包括: 多线程支持, 泛型编程支持, 统一的初始化和提高性能.
这篇文章将核心语言的特性和变化大致分为: 提高运行期性能, 提高编译期性能, 增强可用性, 和新特性4大类. 某些特性可以被划分到多个分类中, 但只会在主要体现该特性的分类中讨论一次.
3.提高核心语言运行性能
以下特性主要是为了提高性能提或计算速度等设计的.
3.1 右值引用和移动构造
在C++03及之前的标准中,临时对象(称为右值"R-values",因为通常位于赋值运算符的右边)的值是不能改变的,和C语言一样, 且无法和 const T& 类型做出区分。尽管在某些情况下临时对象的确会被改变,甚至有时还被视为是一个有用的漏洞。C++11新增加了一个非常量引用(non-const reference)类型,称作右值引用(R-value reference),标记为T &&。右值引用所引用的临时对象可以在该临时对象被初始化之后做修改,这是为了允许 move 语义。
C++03 性能上被长期诟病的问题之一,就是其耗时且不必要的深拷贝。深拷贝会隐式地发生在以传值的方式传递对象的时候。例如,std::vector内部封装了一个C风格的数组和其元素个数,如果创建或是从函数返回一个std::vector的临时对象,要将其保存起来只能通过生成新的std::vector对象并且把该临时对象所有的数据复制进去。该临时对象和其拥有的內存会被销毁。(为简单起见,这里忽略了编译器的返回值优化)
在 C++11中,std::vector有一个"移动构造函数",对某个vector的右值引用可以单纯地从右值复制其内部C风格数组的指针到新的vector中,然后将右值中的指针置空。因为这个临时对象不会再被使用,没代码会再访问这个空指针, 而且因为这个临时对象的内部指针是NULL,所以当这个临时对象离开作用域时它的内存也不会被释放掉.所以,这个操作不仅没有代价高昂的深拷贝, 还是安全的,对用户不可见的!
这个操作不需要数组的复制,而且空的临时对象的析构也不会销毁内存。返回vector临时对象的函数只需要返回std::vector
右值引用不用对标准库之外的代码做任何改动就可以为已有代码带来性能上的提升. 返回值类型为std::vector
出于安全考虑, 需要施加一些限制! 一个已命名的变量即使声明为右值,也不会被视为右值.想要获取一个右值,应该使用模板函数std::move
由于"右值引用"这个词的自然语义,以及对"左值引用"(常规引用)这个词的修正, 右值引用可以让开发者提供完美的函数转发! 与可变参数模板结合时, 这个能力让模板函数能够完美地将参数转发给带有这些参数的另一个函数.这对构造函数的参数转发最为有用,创建一个能够根据特定的参数自动调用适当的构造函数的工厂函数.
3.2 泛化的常数表达式
C++一直以来都有常量表达式的概念.这种表达式就像3+4这种在编译期和运行时都能得到相同结果的表达式. 常量表达式给编译器提供了优化的机会, 编译器计算出他们的值并把结果硬编码到程序中. 并且C++规格文档中有很多地方要求使用常量表达式. 例如,定义一个数组需要常量表达式(来指定数组大小), 枚举值必须是常量表达式.
然而,常量表达式中从来都不允许调用函数或创建对象. 所以,像下面这样的简单代码却是非法的:
int get_five() {return 5;}
int some_value[get_five() + 7]; // 创建一个包含12个整数的数组. 这种形式在C++中是非法的.
这在C++03中是非法的, 因为get_five() + 7不是常量表达式. C++03的编译器在编译期没办法知道get_five()是常量.因为从理论上讲, 这个函数可以影响(改变)一个全局变量或调用其它非运行时常量函数等.
C++11引入了constexpr关键字, 允许用户去保证一个函数或对象的构造函数是一个编译期常量.上面的例子可以写成下面这样:
constexpr int get_five() {return 5;}
int some_value[get_five() + 7]; // 创建一个包含12个整数的数组. 这种形式在C++11中是合法的.
这样可以让编译器理解并验证get_five()是一个编译期常量!作用在函数上的constexpr关键字对函数的行为施加了一些限制. 首先, 这个函数的返回值类型不能是void; 其次, 在函数体中不能声明变量或新类型; 第三, 函数体内只能包含声明语句,空语句和单个return语句且,return语句中的表达式也必须是常量表达式.
在c++11之前, 变量的值只有在变量被声明为const类型,有常量表达式初始化它, 并且是整型或枚举类型时才能用在常量表达式中. C++11去掉必须是整数或枚举的限制,如果定义变量时用了constexpr关键字:
constexpr double earth_gravitational_acceleration = 9.8;
constexpr double moon_gravitational_acceleration = earth_gravitational_acceleration / 6.0;
这种数据变量是隐式的常量, 必须用常量表达式初始化.想要构造用户定义类型的常量值,构造函数也必须用constexpr声明.
3.3 对POD定义的修正 在C++03中, 类或结构体必须遵守几条规则才能认为是POD类型.符合这种定义的类型能够产生与C兼容的对象(内存)布局, 而且可以被静态初始化.C++03标准对与C兼容或可以静态初始化的类型有严格的限制,尽管不是因技术原因导致编译器不接受这样的编码.如果创建了一个C++03的POD类型,又想要增加一个非虚成员函数, 那么这个类型就不再是POD类型了, 不能静态初始化,并且与C不兼容了, 尽管没有改变内存布局.
C++11将POD的概念拆分成了两个独立的概念:平凡的(trivial)和标准布局(standard-layout), 放宽了几条POD规则. 一个平凡的(trivial)类型可以静态初始化. 这意味着可以通过memcpy来拷贝数据,而不必通过拷贝构造函数. 平凡类型的变量的生命期是从分配存储空间开始的,而不是从构造完成开始.
平凡的类和结构体定义如下:
1 有一个平凡的默认构造函数. 这可以使用默认构造函数语法, 例如SomeConstructor() = default;
2 有平凡的拷贝和移动构造函数, 可以使用默认语法.
3 有平凡的拷贝和移动赋值运算符, 可以使用转变语法.
4 有一个平凡的析构函数, 必须是非虚函数.
只有当一个类没有虚函数,没虚基类时它的构造函数才是平凡的. 拷贝和移动操作还要求类的所有非静态数据成员都是平凡的.
一个类型是标准布局的,就意味着它将以与C兼容的方式来排列和打包它的成员.标准布局的类和结构体定义如下:
1 没有虚函数
2 没有虚基类
3 所有的非静态数据成员都有相同的访问控制 (public, private, protected)
4 所有的非静态数据成员, 包括基类中的, 都要在继承体系中的同一个类中.
5 以上规则也适用于类体系中的所有基类和所有非静态数据成员.
6 没有和第一个定义的静态数据成员相同类型的基类
如果一个类型是平凡的(trivial),是标准布局的, 并且所有的非静态数据成员和基类都是POD类型的, 那么这个类型就是POD类型.
4 核心语言建构期性能提高
4.1 外部模板 在C++03中,只要在编译单元内遇到被完整定义的模板,编译器都必须将其实例化(instantiate)。这会大大增加编译时间,特别是模板在许多编译单元内使用相同的参数实例化。没有办法告诉C++不要引发模板的实例化.C++11引入了外部模板声明, 就像外部数据声明一样. C++03用下面的语法迫使编译器实例化一个模板:
template class std::vector<MyClass>;
而C++11提供下面的语法告诉编译器在当前编译单元中不要实例化这个模板:
extern template class std::vector<MyClass>;
5 核心语言可用性的增强
这些特性存在的主要目的是为了让C++更使用. 这些特性可以改进类型安全, 最小化代码重复, 尽可能减少错误代码等.
5.1 初始化列表
C++03从C语言继承了初始化列表这一特性. 在一对大括号中列出参数的方式来给出一个结构体或者数组, 这些参数值按照各个成员在结构体中的定义顺序来排列.这些初始化列表是递归的,所以一个结构体数组或包含另一个结构体的结构体可以使用它们.
这对于静态初列表或者只想把结构体初始化为某个特定值而言是非常有用的. C++提供了构造函数来初始化对象, 但这没有初始化列表方便.C++03只允许符合POD定义的类型使用初始化列表,非POD的类型不能使用,就连相当有用的STL容器std::vector也不行.C++11扩展了初始化列表, 使用它可以用在所有类上,包括像vector这样的标准容器.
C++11把初始化列表的概念绑到一个叫做std::initializer_list的模板上.这允许构造函数或其他函数将初始化列表做为参数.例如:
这使得可以从一串整数来创建SequenceClass对象, 例如:
SequenceClass some_var = {1, 4, 5, 6};
这个构造函数是一种特殊的构造函数,叫做初始化列表构造函数(initializer-list-constructor).有这种构造函数的类在统一初始化中会被特殊对待(详见5.2)
类型std::initializer_list<>是个第一级的C++11标准程序库类型. 但是,它们只能由C++11通过{}语法来静态构造!这个列表一经构造便可复制,虽然这只是copy-by-reference.初始化列表是常数;一旦被创建,其成员均不能被改变,成员中的数据也不能够被改变.
因为初始化列表是真实类型,除了构造函数之外还能够被用在其他地方。常规的函数能够使用初始化列表作为参数。例如:
5.2 统一的初始化
C+03在初始化类型方面有着许多问题.初始化类型有数种方法,而且交换使用时不会都产生相同结果。传统的建构式语法,看起来像是函数声明,而且必须采取一些步骤保证不破坏编译器那些最让人恼火的解析规则.只有聚合体和POD类型能够用集合式初始化(通过SomeType var = {}; 形式的语法)
C++11提供了一个完全统一的可以用在任何类型的对象的初始化语法. 它扩展了初始化列表语法:
var1初始化行为就像聚合初始化一样.也就是说,每个数据成员就是一个对对象, 按顺序从初始化列表中拷贝一个对应的值来初始化它们.如果有需要, 会进行隐式类型转换.如果存在向下类型转换(转换后的数据类型不能表示原数据类型,转换后可能有数据丢失,例如将unsigned转换成int), 那么这个程序就是病态的,会导致编译失败. var2的初始化则是简单地调用构造函数.
统一的初始化还可做下面这件事:
统一初始化不会取代构造函数语法,还是有一些时候是需要构造函数语法的.如果一个类有初始化列表构造函数(TypeName(initializer_list);),假定它有资格成为构造函数之一(我们知道,一个类可以有多个构造函数),那么它的优先级会高于其它形式的构造函数.C++11版本的std::vector就有一个初始化列表构造函数.这意味着 std::vector the_vec{4};会调用初始化列表构造函数,而不是调用以vector大小为唯一参数的构造函数. 要访问后一个构造函数, 用户必须直接使用标准构造函数语法.
5.3 类型推导
在C++03(还有C)中,必须显式指定变量的类型.然而,随着模板类型和模板元编程技术的出现,某些东西的类型,尤其是函数的返回类型,可能不是那么容易表示的了. 在这种情况下,将中间结果存储在某个变量中是件很困难的事情.可能需要去了解特定的模板元编程库的内部实现.
C++11提供两种方法来缓解上述问题. 一,定义有显式初始化的变量可以用auto关键字来自动确定变量类型,这将用初始化表达式的类型来创建变量:
some_strange_callable_type的类型很简单, 就是boost::bind模板函数返回值的类型.作为编译器语义分析责任的一部份,编译很容易确定这个类型,但程序员就没那么容易确定了.otherVariable 的类型同样也是定义明确的,程序员很容易就能判别。它是个int(整数),就和整数字面值的类型一样。
另外,关键字decltype可以用来在编译期确定表达式的类型.例如:
decltype 和 auto 一起使用会更为有用,因为 auto 参数的类型只有编译器知道.然而 decltype对于那些大量运用运算符重载和类型特化来编码的表达式非常有用。auto对减少代码冗余也很有用.比如说, 程序员不用像下面这样写代码:
这两种形式的差距会随着你使用的容器的嵌套层次而增加, 这种情况下typedef也是一种减少代码的好方法!由decltype得出的类型可以和由auto推导出的类型不同:
5.4 基于范围的for循环
在C++03中,要遍历一个list中的元素需要很多代码.其它语言实现支持"糖块句法",允许程序通过一个简单的"foreach"语句自动遍历list中的元素.其中之一就是Java语言, 它从5.0开始支持增强的for循环.
C++11增加了一个类似的特性, for语句可以简单地遍历列表中的元素.
这种形式的for语句叫作"基于范围的for语句",它会遍历列表中的每一个元素.可以用在C风格数组,初始化列表和那些带有能返回迭代器的begin()和end()函数的类型上.所有提供了begin/end的标准容器都可以使用基于范围的for语句.
5.5 Lamda函数与表达式
C++11提供了创建匿名函数的能力,叫做Lamda函数. 具体内容请参考: http://www.cnblogs.com/pzhfei/archive/2013/01/14/lambda_expression.html
5.6 一种新的函数语法
标准C的函数声明语法对C语言的特性集而言完全足够了. 因为C++从C发展而来, 保留了C的基本语法并在需要的地方进行了扩展. 然而,C++的结构变得更加复杂了,暴露出了很多的局限性,尤其是模板函数的声明.下面的例子在C++03中是不允许的:
Ret的类型是lhs+rhs的结果的类型.就算用前面提到的C++11中的decltype,也是不行的:
这不是合法的C++,因为lhs和rhs还没定义;解析器解析完函数原型的剩余部分之前,它们还不是有效的标识符.
为此, C++11引入了一种新的函数声明语法,叫做后置返回类型(trailing-return-type).
这种语法可以用到更普通的函数声明和定义上:
关键字auto的这种用法与在自动类型推导中有所不同.
5.7 对象构造的改进
C++03中类的构造函数不允许调用该类的其它构造函数;每个构造函数都必须自己或者调用一个公共的成员函数来构造类的全部成员.例如:
而且,基类的构造函数不能直接暴露给派生类;每个派生类必须实现自己的构造函数哪怕基类的构造函数已经够用了.非静态数据成员不能在声明的地方初始化.它们只能在构造函数中初始化.
C++11为这些问题提供了解决方案.C++11允许构造函数调用另一个构造函数(叫做委托构造).这允许构造函数利用其它构造函数的行为而只需增加少量的代码.C#,java和D语言都提供了这种功能. C++的语法如下:
注意:这个例子可以通过给new_number设定一个默认参数来达到相同的效果.但是,这种新语法可以让这个默认值在实现中来设置而不是在接口中设置.这带来的一个好处就是,对库代码的维护者而言,在接口中(头文件中)声明默认值,这个默认值被嵌入到了调用端;要改变这个默认值的话,调用端的代码都需要重新编译.但委托构造可以在实现中(CPP文件中)来改变这个默认值, 这样调用端的代码就不需要重新编译,只用重新编译这个库就可以了.
还有一个附加说明: C++03认为,构造函数执行完了一个对象就被构造好了. 而C++11则认为,只要任何一个构造函数执行完了,对象就算构造完成了. 由于可能有多构造函数会被执行,C++11的这种做法就意味着,所有的委托构造函数都将在一个已经用它自己类型完全构造好的对象上执行.这句话什么意思呢?举个例子, B 继承自 A, B的构造函数里调用了A的构造函数;当A的构造函数执行完以后,就已经有一个A类的对象构造完成了.而这时B的构造函数不会再构造一个新对象,而是把那个A对象改造成B类的对象(这是我的推测).再举一个例子,类C有两个构造函数C1和C2, C2调用了C1. 当C1执行完后,已构造好了一个C类对象.而这时C2的代码会直接作用在这个对象上,不会再构造一个新对象.C++03就会构造2个对象,其中一个是临时对象.
对于基类的构造函数,C++11允许一个类指定要不要继承基类的构造函数.注意,这是一个"全部"或"全不"的特性,要么继承基类的全部构造函数,要么一个都不继承.
此外,对多重继承有一些限制,从多个基类继承而来的构造函数不可以有相同的函数签名(signature).而派生类的新加入的构造函数也不可以和继承而来的基类构造函数有相同的函数签名,因为这相当于重复声明.语法如下:
每一个构造函数都将把value初始化为5, 如果它们没用其它值来覆盖这个初始化的话.上面那个空的构造函数会把value初始化为类定义时的状态5.而那带有参数的构造函数会用指定的值来初始化value.成员的初始化也可以使用前面提到的统一初始化.
5.8 显式重写(覆盖,override)和final
在C++03中,很容易让你在本想重写基类某个函数的时候却意外地创建了另一个虚函数.例如:
本来Derived::some_func函数是想替代Base中那个函数的.但是因它的接口不同, 又创建了一个虚函数.这是个常见的问题, 特别是当用户想要修改基类的时候.
C++11引入了新的语法来解决这个问题:
override 这个特殊的标识符意味编译器将去检查基类中有没有一个具有相同签名的虚函数,如果没有,编译器就会报错!
C++11还增加了防止基类被继承和防止子类重写函数的能力.这是由特殊的标识符final来完成的,例如:
在这个例子中, virtual void f() final;语句声明了一个虚函数却也阻止了子类重写这个函数.它还有一个作用,就是防止了子类将那个特殊的函数名与新的参数组合在一起.
需要注意的是,override和final都不是C++语言的关键字.他们是技术上的标识符,只有在它们被用在上面这些特定的上下文在才有特殊意义.用在其它地方他们仍然是有效标识符.
5.9 空指针常量
本节中出的"0"都将解释为"一个求值结果为0的int型常量表达式". 实际上任何整数类型都可以作为常量表达式.
自从1972年C诞生以来,常量0就有着int型常量和空指针的双重角色.C语言用预处理宏NULL来处理这个固有的歧义, NULL通常被定义为(void*)0或0.而C++不采用同样行为,只允许0做空指针常量.而这与函数重载配合时就显得有些弱智了.
如果NULL定义为0,那么foo(NULL);语句将会调用foo(int).这几乎必定不是程序员想要的,也不是代码直观上要表达的意图.
C++11通过引入一个新的关键字nullptr充当单独的空指针常量来纠正这个问题.它的类型是nullptr_t,是一个可以隐式转换任意类型的指针或指向成员的指针的类型,并且可以和这些类型进行比较.它不能隐式转换为整型,也不能与整型做比较,bool类型除外.尽管最初的提议中一个nullptr类型的右值不应该能转换为bool类型,但是为了保持与常规指针类型的一致性,核心语言工作组还是认定这种转换是合理的. 为了向下兼容,0仍然是一个有效的空指针常量!
5.10 强类型枚举
在C++03中,枚举不是类型安全的.他们实际上是整数,尽管他们是不同的枚举类型.这使得我们可以比较两种不同类型的枚举值.C++03提供的唯一安全性就是,一个整数或一个枚举类型的值不能隐式地转换成另一个枚举类型.此外,底层的具体的整数类型(short,long,int,...)是由实现(编译器)定义的,标准并无明确规定.因此,那些枚举变量的大小的代码将是不可移植的.最后,枚举值是暴露在外层作用域(直接包含枚举定义的作用域)中的.所以,两个不同枚举类型的成员不可能有相同的名字.
C++11引入了一个没上述问题的特殊"枚举类".使用 enum class(也可以用同义词enum struct)来声明:
这种枚举是类型安全的;枚举值不能隐式地转换成整数,所以也不可以和整数做比较.表达式 Enumeration::Val4 == 101会报一个编译错误.
枚举类的底层类型总是已知的.默认是int型,这可以用其它整数类型来覆盖它.就像下面这个例子:
老式的枚举被放在直接包含该定义的作用域中.新式的枚举被放在枚举类的作用中.所以,上例中Val1是未定义的,而Enum2::Val1是已定义的.
C++11还提供了一个过渡语法让老式的枚举类型可以提供显式的作用域和定义底层整数类型.语法如下:
这个例子中枚举名字被定义在枚举类型的作用域内(Enum3::Val1),但是为了向下兼容它们也会被放在直接包含在Enum3所在的作用域中.
5.11 右尖括号
C++03的解析器都把">>"定义为右移运算符.但是,在嵌套的模板声明中,程序员往往倾向于忽略两个右尖括号之间的空格.这会导致编译器报一个语法错误.
C++11改进了编译器的解析规则,尽可能地将多个右尖括号(>)解析成模板参数结束符.可以用圆括号来改变这个规则,圆号的优先级比它高.例如:
5.12 显式类型转换操作符
C++98 增加了explicit关键字来防止单参数的构造函数被用作隐式的类型转换操作符.然而,却没有对真正的类型转换操作符这样做.例如,智能指针可能定义了operator bool(),让它的行为更像真原始指针.有了这个转换操作符就可以用if语句来测试:if (smart_ptr_variable),当这个指针不为空时结果为真,否则为假.但是,这也会引起其他一些非预期的转换操作.因C++中的bool被定义为一种算术类型,可以隐式地转换为整数甚至是浮点数进行数学运算. 拿转换出的布尔值进行非布尔计算的数学计算,往往不是程序员想要的.
C++11中,explicit关键字也可以用在类型转换操作符上.和用在构造函数上一样,防止这种转换函数被于隐式类型转换.当然了,在语言的上下文明确要求使用布尔变量(如if语句和循环语句中的条件,还有逻辑运算符的操作数)的地方,会被当作显示转换.所以可以使用类型转换操作符.
5.13 模板的别名
在进入这个主题前,先弄清楚"模板"和"类型"的区别.类型,是具体的数据类型,可以直接用来定义变量. 模板,是类型的模板,根据这个模板可以产生具体的类型;模板是不能直接定义变量的;当指定了所有的模板参数后,就产生了一个具体的类型,就可以用来定义变量了.
在C++03中,只能为类型(包括完全特化的模板,也是一种类型)定义别名, 而不能为模板定义别名:
C++11增加为模板定义别名的能力,用下面这样的语法:
这种using语法也可以用来定义类型的别名:
5.14 无限制的unions
C++03中,对哪些类型的对象能够作为联合的成员是有限制的.例如,联合不能包含定义了非平凡构造函数的对象.C++11废除了其中的一些限制:
现在,联合可以包含定义了非平凡构造函数的对象;如果包含了,那么联合就必须要显式定义一个构造函数.
因为是放宽了现有的规则,所以不会对已有的代码造成影响.
6 核心语言功能的改进
这些特性让C++语言可以完成那些以前不可能的,极其繁琐的或者需要一些不可移植的库才能完成的事情.
6.1 可变参数模板
在 C++11 之前, 不论是类模板或是函数模板,都只能按其被声明时所指定的样子,接受一组数目固定的模板参数.C++11 加入新的表示法,允许任意个数,任意类型的模板参数,不必在定义时将参数的个数固定。
模板类 tuple 的对象,能接受不限个数的 typename 作为它的模板形参:
实参的个数也可以是0,所以 class tuple<> someInstanceName 这样的定义也是可以的。
若不希望产生实参个数为 0 的变长参数模板,则可以采用以下的定义:
变长参数模板也能运用到函数模板上。传统 C 中的 printf 函数,虽然也能做到不定个数的形参来调用,但其并非类型安全的。以下的例子中,C++11 除了能定义类型安全的变长参数函数外,还能让类似 printf 的函数能自然地处理自定义类型的对象。 除了在模板参数中能使用...表示不定长模板参数外,函数参数也使用同样的表示法代表不定长参数。
其中,Params 与 parameters 分别代表模板与函数的变长参数集合,称之为参数包 (parameter pack).参数包必须要和运算符"..."搭配使用,避免语法上的歧义。
变长参数模板中,无法像在类或函数中那样使用参数包.因此典型的做法是以递归的方法取出可用参数,请看以下的 C++11 printf 例子:
printf 会不断地递归调用自身:函数参数包 args... 在调用时, 会被模板类匹配分离为 T value和 Args... args.直到 args... 变为空参数,则会与简单的 printf(const char *s) 形成匹配,退出递归。
另一个例子为计算模板参数的个数,这里使用相似的技巧展开模板参数包 Args...:
虽然没有一个简洁的机制能够对变长参数模板中的值进行迭代,但使用运算符"..."还能在代码各处对参数包施以更复杂的展开操作。举例来说,一个模板类的定义如下:
BaseClasses... 会被展开成类型 ClassName 的基底类; ClassName 的构造函数需要所有基类的左值引用,而每一个基类都是以传入的参数做初始化 (BaseClasses(baseClasses)...)。
在函数模板中,变长参数可以和左值引用搭配,达成形参的完美转发 (perfect forwarding):
参数包 parms 可展开为 TypeToConstruct 构造函数的形参。 表达式std::forward
此外,变长参数的数量可以藉以下的语法得知:
SomeStruct
6.2 新的字符串字面值
C++03提供两种字符串字面值.第一种,包含在一对双引号内,产生一个以空字符结尾的const char数组.第二种,由L""定义,产生以空字符结尾的const wchar_t类型的数组.wchar_t是一个大小和定义都未明确定义的宽字符.字符串字面值既不支持UTF-8,UTF-16也不支持其它任何类型的unicode编码.
char类型的定义被修改了,明确表述为:char的大小至少能存储UTF-8的8位编码,并且要足够大到能够存储编译器实际使用的字符集的任何成员.这以前只在C++标准的后半部分中有定义,依靠C标准来保证char的大小至少为8位.
C++11支持3种UNICODE编码: UTF-8, UTF-16, 和 UTF-32.除了前面提到的char类型定义的修改,C++11还增加了两种字符类型:char16_t 和 char32_t.这两种类型是分别用来存储UTF-16和UTF-32的.
下面的例子展示了如何创建各种编码类型的字符串字面值:
u8"I'm a UTF-8 string."
u"This is a UTF-16 string."
U"This is a UTF-32 string."
第一个字符串的类型是const char[], 第二个字符串的类型是const char16_t[], 第三个字符串的类型是const char32_t[]. 创建Unicode字符串时,经常会直接插入Unicode编码值到字符串中.为此,C++11提供如下语法:
u8"This is a Unicode Character: \u2018."
u"This is a bigger Unicode Character: \u2018."
U"This is a Unicode Character: \U00002018."
'\u'后面是一个16进制数字,不需要加0x前缀.标识符\u代表一个16位的Unicode码点.要输入32位的码点,使用\U加上32位的16进制数.只能输入有效的Unicode码点.例如,U+D800—U+DFFF之前的码点是被禁止的,因为他们被保留用作UTF-16编码中的代理对.
有时候我们需要手动避免转义某些字符,尤其是在使用xml文件,脚本语言或正则表达式等的字符串字面值时.C++提供了原始字符串:
R"(The String Data \ Stuff " )"
R"delimiter(The String Data \ Stuff " )delimiter"
6.3 用户自定义的字面值
C++03提供几种字面值.字符串"12.5"会被编译器解析为double类型的值12.5. 但是,带有'f'后缀的字符串"12.5f"会创建一个float类型的值12.5. 后缀修饰符已经被C++标准固定下来了, 用户代码不能增加新的修饰符!
C++11增加了让用户定义新的字面值修饰符的能力, 新的修饰符会基于被修饰的字符串来构造对象.
字面值的转换可以分为两个阶段:原始的和转换后的(raw and cooked).原始的字面值是指某种特定类型的字符序列, 而加工过的字面值则代表另一种类型.C++的字面值1234, 原始的字面值就是字符序列'1','2','3','4';而转换后的字面值是整数1234. C++字面值0xA,转换前的字面值是'0','x','A',转换后就是整数10.字面值原始和转换后的形式都可以被扩展.但字符串除外,它只有转换后的形式可以被扩展.这个例外是因为考虑到字符串有着会影响字符的特定意义和类型的前缀. 所有的用户自定义字面值都是加后缀的, 想定义加前缀的字面值是不可能的.
自定义字面值原始形式的处理定义如下:
OutputType operator "" _suffix(const char * literal_string);
OutputType some_variable = 1234_suffix;
第二个语句执行由自定义字面值定义的函数代码.
另一种处理整数和浮点数原始字面值的机制是通过可变参数模板:
template<char...> OutputType operator "" _tuffix();
OutputType some_variable = 1234_tuffix;
OutputType another_variable = 2.17_tuffix;
这个例子展示了如何以operator "" _tuffix<'1', '2', '3', '4'>()函数处理字面值.这种形式中,字符串没有结尾的空字符!这么做的主要目的是为了使用C++11的constexpr关键字和编译器能在编译期就完全转换这些字面值.
对于数字字面值,整数字面值转换后的类型是unsigned long long,浮点数字面转换后的类型是long double.没有模板形式:
OutputType operator "" _suffix(unsigned long long);
OutputType operator "" _suffix(long double);
OutputType some_variable = 1234_suffix; // Uses the 'unsigned long long' overload.
OutputType another_variable = 3.1416_suffix; // Uses the 'long double' overload.
对于字符串字面值,下面的例子用了前面提到的新的字符串后缀.
OutputType operator "" _ssuffix(const char * string_values, size_t num_chars);
OutputType operator "" _ssuffix(const wchar_t * string_values, size_t num_chars);
OutputType operator "" _ssuffix(const char16_t * string_values, size_t num_chars);
OutputType operator "" _ssuffix(const char32_t * string_values, size_t num_chars);
OutputType some_variable = "1234"_ssuffix; // Uses the 'const char *' overload.
OutputType some_variable = u8"1234"_ssuffix; // Uses the 'const char *' overload.
OutputType some_variable = L"1234"_ssuffix; // Uses the 'const wchar_t *' overload.
OutputType some_variable = u"1234"_ssuffix; // Uses the 'const char16_t *' overload.
OutputType some_variable = U"1234"_ssuffix; // Uses the 'const char32_t *' overload.
6.4 多线程内存模型
内存模型允许编译器完成很重要的优化.即使像移动程序中的语句来合并循环这样简单的编译器优化都能够影响对潜在共享变量读,写操作的顺序!改变读写顺序会导致竞态条件的产生.没有内存模型,编译器一般不能将这种优化应用到多线程程序中的,或者只能用于某些特殊情况.现代程序设计语言,比如Java,为此实现了一个内存模型.内存模型指定了同步屏障(Synchronization Barriers),通过特殊的、定义好的同步操作(比如获得一个进入同步块或某方法的锁)来建立的.内存模型规定,共享变量值的改变只需要对那些通过了同步屏障的线程是可见的.此外,竞态条件这个概念的完整定义覆盖了带有内存屏障细节的操作顺序. 这些语义给了编译器更高的自由度去进行优化: 编译器只需要确保优化前和优化后同步屏障内的变量(可能被共享)的值是一样的.
大多数关于内存模型的研究都是围绕着以下主题进行的:
设计一个能让编译器有最大的优化自由度,同时还能对自由竞争提供足够保障的内存模型;
提供关于这种内存模型的正确的程序优化.
C++11标准支持多线程编程.这包含两个部分:同一个程序中允许有多个线程同时存在和库支持线程之间的交互;内存模型定义了什么时候多个线程能够访问同一个内存地址,并指定了什么时候一个线程对内存的修改对另一个线程是可见的!(参见:7.2 线程设施).
6.5 线程局部存储
在多线程环境中,线程通常都有一些自己所独有的变量. 函数的局部变量也是这样, 但是全局变量和静态变量就不一样了.
新的线程局部存储的生存期(原有的静态,动态,自动变量除外)由thread_local关键字指定. 静态对象的生存期也可能会被thread-local生存期替代.这么做的目的是让thread-local(线程局部)生存期的对象可以像其他静态对象一样由构造函数创建,由析构函数销毁.
6.6 明确默认和明确删除的特殊成员函数
C++03中,如果类没有定义构造函数,拷贝构造函数,赋值函数和析构函数的话,编译器会为类提供这些函数.程序员可以自己定义这些函数来覆盖编译生成的默认版本.C++还定义了几个可以作用在所有类上的操作符(比如,赋值操作符=,new操作符等),程序员也可以覆盖它们.
然而, 对这些默认函数的创建只有很少的控制.例如, 要生成一个不可拷贝的类必须要声明私有的拷贝构造函数和私有的赋值操作符并且不定义它们的实现.试图调用这些函数就会违反"一个定义原则"(ODR,一个函数可以被调用,那么这个函数必须且只能有一个函数体定义).尽管诊断信息不是必须的,但是这类违规行为可能会导致链接错误.
就构造函数而言, 只要一个类定义了任意一个构造函数,编译器就不会自动为它生成构造函数了.这在很多情况下是很有用的,但有些情况下用户定义了这些函数,编译器还生成这些函数也是很有用的.
C++11允许显式指明要不要使用这些特殊的成员函数.例如,下面的声明显式指出要使用默认构造函数:
struct SomeType {
SomeType() = default; //The default constructor is explicitly stated.
SomeType(OtherType value);
};
另一方面,一些特性可以被显式地禁用.例如,下面的类是不可拷贝的:
struct NonCopyable {
NonCopyable() = default;
NonCopyable(const NonCopyable&) = delete;
NonCopyable & operator=(const NonCopyable&) = delete;
};
指示符 = delete 可以用来阻止任何函数被调用,可以用来禁止调用带特定参数的成员函数.例如:
struct NoInt {
void f(double i);
void f(int) = delete; // 不能调用这个函数
};
编译器会拒绝试图对带int参数的函数f()的调用, 而不是默默地转换为对带有double参数的f()的调用.这可以泛化到禁止除了带double参数外其他任何参数类型的f()的调用.例如:
struct OnlyDouble {
void f(double d);
template<class T> void f(T) = delete; //不能调用这个函数
};
6.7 long long int类型
C++03中,最大的整数类型是long int.它保证使用的位数至少与int一样. 这导致long int在一些实现是64位的, 而在另一些实现上却是32位的.C++11增加了一个新的整数类型long long int来弥补这个缺陷.它保证至少与long int一样大,并且不少于64位.这个类型早在C99就引入到了标准C中, 而且大多数C++编译器都以扩展的形式支持这种类型了.
6.8 静态断言
C++03提供两种方法来测试断言:宏assert和#error预处理指令.然而,这不适合用在模板中:宏在运行期间测试断言,而预处理指令在编译预处理阶段测试断言,这些都发生在模板实例化之前;也不适合用于依赖于模板参数的属性.
C++11引入了一个新的关键字static_assert在编译期测试断言.声明呈现下面这样的形式:
static_assert (constant-expression, error-message);
//下面几个例子展示怎样使用static_assert:
static_assert((GREEKPI > 3.14) && (GREEKPI < 3.15), "GREEKPI is inaccurate!");
template<class T>
struct Check {
static_assert(sizeof(int) <= sizeof(T), "T is not big enough!");
};
template<class Integral>
Integral foo(Integral x, Integral y) {
static_assert(std::is_integral::value, "foo() parameter must be an integral type.");
}
当常量表达的结果为false时,编译器就会产生一个错误消息.第一个例子类似于预处理指令#error,但是预处理指令只支持整数类型.相比之下,第二例子中的断言在模板类Check每一次被实例化的时候都被检查一次.
除了模板之外,静态断言也是很有用的.例如:一个算法的某个实现依赖于long long类型必须大于int,这类型事情标准并没有做出保证.这种假设在大多数系统和编译器上是有效的,但绝不是全部!
6.9 允许sizeof运算符作用在类型的数据成员上,无须明确的对象
C++03中,sizeof可以作用在类和对象上.但却不能像下面这样做:
struct SomeType { OtherType member; };
sizeof(SomeType::member); // C++03 不行. C++11 可以.
//这会返回OtherType的大小.C++03不允许这样做,会报一个编译错误.C++11允许这样做.
6.10 控制和查询对象的对齐方式
C++11可以用alignof和alingas来查询和控制变量的对齐方式.
alignof是一个操作符,他以一个类型为参数,并且返回这个类型的实例必须分配的字节边界值,这个值一定是2的整数次幂.如果参数是引用类型,那么返回的是被引用的类型的对齐信息.对于数组,返回的是元素类型的对齐信息.
laignas指示符变量的内存控制方式.这个指示符的参数是一个常量或一个类型, alignas(T)是alignas(alignof(T))的简写形式.例如:下面的例子声明一个char数组,它的对齐方式与float型数据一样.
alignas(float) unsigned char c[sizeof(float)]
6.11 允许实现垃圾回收
之前版本的C++标准通过set_new_handler提供了程序员驱动的垃圾回收机制,但却没有为自动化垃圾回收机制给出对象可到达性的定义. C++11定义了指针完全地从其他地方获得值的条件.编译器实现可以指定在严格的指针安全下进行操作,在这种情况下不按这个规则获得值的指针就会变成无效的.
6.12 属性
C++11为编译器和其他工具提供了标准的语言扩展语法.这些扩展历来都是用#pragma指令或生产商指定的关键字(如GNU的__attributes__和微软的 __declspec).C++11有了新的语法, 以双重方括号的形式为属性指定额外的信息.属性可以被用于各种代码元素:
int [[attr1]] i [[attr2, attr3]];
[[attr4(arg1, arg2)]] if (cond)
{
[[vendor::attr5]] return i;
}
在上面的例子中,属性attr1作用在变量i的类型int上,而attr2和attr3则作用于变量i本身.attr4作用于if语句,vendor::attr5作用于return语句.一般地(但有一些例外),为一个命名实体指定的属性放在实体名字之后,其他部分之前.多个属性可以放在一个双重方括号对中,像上面的例子那样. 属性可能会有附加的参数,属性也可能被放在生产商指定的属性命名空间中.
建议属性不要有任何语言上的意义,也不要改变程序的观感. 属性可以提供一些很有用的信息,例如帮助编译器生成更好的诊断信息或优化生成的代码.
C++11本身提供两种标准的属性:noreturn属性指出函数没有返回值, carries_dependency属性通过指出函数的参数或返回值有依赖关系来帮助优化多线程代码.
7. C++标准程序库的变化
C++11标准库引入了很多新特性.很多是在旧标准下实现的,但是有一些却依赖于C++11的核心特性.新标准库的大部分是在2005年公布的C++标准委员会标准库技术报告(tr1)中定义的.各种完全或部分的TR1实现在现行的标准中可以通过命名空间std::tr1来引用了.对于C++11,这些实现被移到了命名空间std中.然而,因为TR1的特性被引入到了C++11的标准库中,所以需要更新它们以适合那些在最初的TR1中不可用的C++11特性.
C++11的标准化已经完成,标准委员会打算创建第二版标准库技术报告(TR2).那些被提议但却没来及加入C++11的库,将会被放入TR2或以后的技术报告中.
7.1 标准库组件的升级
C++11提供了很多现存标准库组件能从中获益的新特性.例如,大多数标准容器都可以从基于移动构造右值引用中获益,不管是快速移动重型容器还是把容器的内容移动到新的内存位置.标准库组件已经用适当的C++11新特性升级过了.包括但不限于以下特性:
7.2 线程支持
C++11虽然从语言上提供了支持线程的内存模型,但主要的支持还是来自标准库.
新的标准库提供了一个线程类(std::thread)来运行一个新线程,它带有一个函数对象参数和一系列可选的传递给函数对象的参数.通过std::thread::join()支持的线程连接操作可以让一个线程直到另一个线程执行完毕才停止.std:thread::native_handle()成员函数提供了对底层本地线程对象的可能且合理的平台相关的操作.
为支持线程同步,标准库增加了互斥体(std::mutex, std::recursive_mutex等)和条件变量(std::condition_variable 和std::condition_variable_any).这些都是通过RAII锁和加锁算法就可以简单使用的.
有时为了高性能或底层工作,要求线程间的通信没有开销巨大的互斥锁.原子操作可以达到这个目的,这可以随意地为一个操作指定最小的内存可见度.显式的内存屏障也可以用于这个目的.
C++11线程库还包含了futures和promises,用于在线程间传递异步结果.并且提供了std::packaged_task来封装可以产生这种异步结果的函数调用.
更高级的线程支持,如线程池,已经决定留待在未来的 Technical Report 加入此类支持。更高级的线程支持不会是 C++11 的一部份,但是其最终实现将建立在目前已有的线程支持之上。std::async 提供了一个简便方法来运行线程,并将线程绑定在 std::future上。用户可以选择一个工作是要在多个线程上异步的运行,还是在一个线程上运行并等待其所需要的数据。默认的情况,实现可以根据底层硬件选择前面两个选项的其中之一。另外在较简单的使用场景下,实现也可以利用线程池提供支持。
7.3 元组类型
元组(tuple)由预先确定数量的多种对象组成.元组可以看作是struct数据成员的泛化.TR1 tuple类型的C++11版本获益于像可变参数模板这样的C++11语言特性.TR1版本的元组需要一个由实现定义的包含的类型的最大数目,而且需要大量的宏技巧来实现.相比之下,C++11版本的不需要显式的实现定义的最大类型数目.尽管编译器有一个内部的模板实例化的最大递归深度,但C++11版的元组不会把它暴露给用户.
用可变参数模板,元组类的定义看上去像下面这样:
template <class ...Types> class tuple;
//下面是定义和使用元组的一个例子:
typedef std::tuple <int, double, long &, const char *> test_tuple;
long lengthy = 12;
test_tuple proof (18, 6.5, lengthy, "Ciao!");
lengthy = std::get<0>(proof); // 把'lengthy' 赋值为18.
std::get<3>(proof) = " Beautiful!"; // 修改元组的第四个元素
可以在定义一个元组时不定义他的内容,不过只有当它的每个元素类型都有默认构造函数时才可以这样做.而且,可以将一个元组赋值给另一个元组:如果两个元组的类型相同,且每种元素类型都有拷贝构造函数;否则,需要右边的元组的元素可以隐式转换成左边元组的对应元素类型或者左边元组的元素类型有合适的构造函数.
typedef std::tuple <int , double, string > tuple_1 t1;
typedef std::tuple <char, short , const char * > tuple_2 t2 ('X', 2, "Hola!");
t1 = t2; // Ok, 前两个元素都是可以转换的.
// 第三个可以用'const char *'来构造一个string对象.
可以对元组类型对象的进行比较运算(当它们拥有同样数量的元素)。此外,C++11 提供两个表达式用来检索元组类型的属性(仅在编译期做此检查)。
std::tuple_size::value //返回元组 T 内的元素个数,
std::tuple_element::type //返回元组 T 内的第 I 个元素的类型
7.4 散列表
在过去,不断有要求想将散列表(无序关系式容器)引进标准库。只因为时间上的限制,散列表才没有被标准库所采纳。虽然,散列表在最糟情况下(如果出现许多冲突 (collision) 的话)在性能上比不过平衡树。但实际运用中,散列表的表现则较好。
因为标准委员会还看不到有任何机会能将开放寻址法标准化,所以目前冲突仅能通过线性链(linear chaining) 的方式来处理。为避免与第三方库发展的散列表发生名称上的冲突,前缀将采用 unordered 而非 hash。
标准库将引进四种散列表,其中差别在于以下两个特性: 是否接受具相同键值的项(Equivalent keys),以及是否会将键值映射到相对应的数据(Associated values).新的标准库增加了以下散列表类型:
散列表类型 | 有无关系值 | 接受相同键值 |
---|---|---|
std::unordered_set |
否 | 否 |
std::unordered_multiset |
否 | 是 |
std::unordered_map |
是 | 否 |
std::unordered_multimap |
是 | 是 |
这些类完全具备容器类需的条件,同时也提供访问其中元素的成员函数: insert, erase, begin, end。
散列表不需要对现有核心语言做扩展(虽然散列表的实现会利用到 C++11 新的语言特性),只会对头文件
7.5 正则表达式
新的标准库定义了一个新的头文件
函数 regex_search 是用来搜索模式的; 若要搜索并替换,则要使用函数 regex_replace,该函数会返回一个新的字符串。算法regex_search 和 regex_replace 接受一个正则表达式(模式)和一个字符串,并将该模式匹配的结果情况存储在 struct match_results对象中。
下面的例子展示了 match_results 的用法:
const char *reg_esp = "[ ,.\\t\\n;:]"; // 列出分隔符.
// 这也可以通过字符串字面值来完成:
// const char *reg_esp = R"([ ,.\t\n;:])";
std::regex rgx(reg_esp); // 'regex' 是模板类'basic_regex'以'char'为类型参数特化的类.
std::cmatch match; // 'cmatch'是模板类'match_results'以'const char *'特化的类.
const char *target = "Unseen University - Ankh-Morpork";
// 找出'target'中所有以'reg_esp'中的字符分隔的单词.
if (std::regex_search(target, match, rgx)) {
// 如果找到了指定的单词
const size_t n = match.size();
for (size_t a = 0; a < n; a++) {
std::string str (match[a].first, match[a].second);
std::cout << str << "\n";
}
}
注意双反斜杠的使用,因为 C++ 将反斜杠作为转义字符使用。但 C++11的原始字符串(raw string)可以用来避免这一问题。库
7.6 通用智能指针
这些指针是由 TR1 智能指针演变而来。注意! 智能指针是类而非一般指针。shared_ptr 是引用计数型(reference-counted) 指针类,其行为与一般 C++ 指针极为相似。在 TR1 的实现中,缺少了一些一般指针所拥有的特性,像是别名或是指针运算。C++11增加了这些特性。以下是一个使用 shared_ptr 的例子:
int main( )
{
std::shared_ptr<double> p_first(new double) ;
{
std::shared_ptr<double> p_copy = p_first ;
*p_copy = 21.2;
} // 此時 'p_copy' 会被销毁,但动态分配的 double 不会被销毁。
return 0; // 此时'p_first'会被销毁,但动态分配的 double也会被销毁(因为不再有指针指向它)。
}
auto_ptr 将会被 C++ 标准所废弃,取而代之的是unique_ptr。 unique_ptr 提供 auto_ptr 大部份特性,但不包括 auto_ptr 的不安全性和隐性的左值转移。不像 auto_ptr,unique_ptr 可以存放在 C++11 提出的那些需要移动语义的容器之中。
7.7 可扩展的随机数生成器
C 标准库允许使用rand函数来生成伪随机数。不过其算法则取决于各程序库开发者。 C++ 直接从 C 继承了这部份,但是 C++11 将会提供产生伪乱数的新方法。C++11 的随机数功能分为两部分: 第一,一个随机数生成引擎,其中包含该生成引擎的状态,用来产生随机数。第二,一个分布,这可以用来决定产生随机数的范围,也可以决定以何种分布方式产生随机数。随机数生成对象即是由随机数生成引擎和分布所构成。
不同于 C 标准库的 rand; 针对产生随机数的机制,C++11 将会提供三种算法,每一种算法都有其强项和弱项:
样板类 | 整数/浮点数 | 品质 | 速度 | 状态数* |
---|---|---|---|---|
linear_congruential | 整数 | 低 | 中等 | 1 |
subtract_with_carry | 两者皆可 | 中等 | 快 | 25 |
mersenne_twister | 整数 | 佳 | 快 | 624 |
C++11 将会提供一些标准分布: uniform_int_distribution (离散型均匀分布),bernoulli_distribution (伯努利分布),geometric_distribution (几何分布), poisson_distribution (卜瓦松分布),binomial_distribution (二项分布),uniform_real_distribution (离散型均匀分布), exponential_distribution (指数分布),normal_distribution (正态分布) 和 gamma_distribution (伽玛分布)。下面的例子展示了一个随机数生成对象如何由生成引擎和分布构成的:
std::uniform_int_distribution<int> distribution(0, 99); // 建立分布,以离散均匀分布方式在0到99之间产生随机数
std::mt19937 engine; // 建立随机数生成引擎
auto generator = std::bind(distribution, engine); // 利用 bind 随机数生成引擎和分布組合成一个随机数生成器
int random = generator(); // 产生随机数
7.8 封装引用
我们可以通过实例化模板类 reference_wrapper 得到一个封装引用 (wrapper reference)。封装引用类似于一般的引用。对于任意对象,我们可以通过模板类 ref 得到一个封装引用 (至于 constant reference 则可通过 cref 得到)。当模板函数需要形参的引用而非其拷贝时封装引用就能派上用场了:
// 此函数将得到r的引用,并将r的值加1.
void f (int &r) { r++; }
// 模板函数
template<class F, class P> void g (F f, P t) { f(t); }
int main()
{
int i = 0 ;
g (f, i) ; // 实例化 'g'
// 'i' 不会被修改
std::cout << i << std::endl; // 输出 0
g (f, std::ref(i)); // 实例化 'g>'
// 'i' 会被修改
std::cout << i << std::endl; // 输出 1
}
这项功能将加入头文件
7.9 多态的函数对象包装器
函数对象的多态包装器(又称多态函数对象包装器)在语义和语法上和函数指针相似,但不像函数指针那么狭隘。只要能被调用,且其参数能与包装器兼容的都能以称之为多态函数对象包装器(函数指针,成员函数指针或仿函数)。
通过以下例子,我们可以了解多态函数对象包装器的特性:
std::function<int (int, int)> func; // 利用模板类 'function'
// 建立包裝器
std::plus<int> add; // 'plus' 被声明 'template T plus( T, T ) ;'
// 因此 'add' 的类型是 'int add( int x, int y )'
func = &add; // 可行。'add' 的型参和回返值类型与 'func' 相符
int a = func (1, 2); // 注意: 若包裝器 'func' 沒有引用到任何函数
// 会抛出 'std::bad_function_call' 异常
std::function<bool (short, short)> func2 ;
if(!func2) { // 因为还给'func2'赋值,此表达式为真
bool adjacent(long x, long y);
func2 = &adjacent ; // 可行。'adjacent' 的型参和回返值类型可通过类型转换与'func2'相符
struct Test {
bool operator()(short x, short y);
};
Test car;
func = std::ref(car); // 模板类 'std::ref' 返回一个struct 'car' 成员函数 'operator()' 的包裝
}
func = func2; // 可行。'func2'的型参和回返值类型可通过类型转换而与'func' 相符
模板类 function 将定义在头文件
7.10 用于元编程的类型特征
编写一个创建或修改其它程序(也可以是程序本身)的程序称为元编程.这种行为可以发生在编译期,也可以发生在运行期.C++标准委员会决定引入一个库,允许在编译期利用模板进行元编程.
以下是一个元编程的例子,基于当前的C++03标准: 模板的递归实例化来计算整数的幂
template<int B, int N> struct Pow { // 递归调用和重新组合. enum{ value = B*Pow1>::value }; }; template< int B > struct Pow0> { // ''N == 0'' 是终止条件. enum{ value = 1 }; }; int quartic_of_three = Pow<3, 4>::value;
很多算法都可以操作不同类型的数据;C++的模板支持泛型编程,并且使代码更加紧凑和有用.然而算法通常都需要知道它正使用的数据的类型.这些信息可以在编译期利用类型特征(type trait)获取到. 类型特征(type traits)可以标识出一个对象的类别和类或结构体的全部特征.type traits定义在一个新头文件
在下面的例子中,模板函数elaborate根据给定的数据类型实例化了一个特定的算法(algorithms.do_it)
// 算法一
template< bool B > struct Algorithm {
template<class T1, class T2> static int do_it (T1 &, T2 &) { /*...*/ }
};
// 算法二
template<> struct Algorithm<true> {
template<class T1, class T2> static int do_it (T1, T2) { /*...*/ }
};
// 根据给定的类型,会自动实例化正确的算法.
template<class T1, class T2>
int elaborate (T1 A, T2 B)
{
// 只有当T1是整数,T2是浮点数是实例化算法二
// 其他情况实例化算法一
return Algorithm::value && std::is_floating_point::value>::do_it( A, B ) ;
}
通过定义在
7.11 用于计算函数对象返回类型的统一方法
要在编译期确定一个模板函数的返回值类型不是那么容易的,特别是当返回类型依赖于函数参数的时候.例如:
struct Clear {
int operator()(int) const; // 参数类型
double operator()(double) const; // 与返回类型相同
};
template <class Obj>
class Calculus {
public:
template<class Arg> Arg operator()(Arg& a) const {
return member(a);
}
private:
Obj member;
};
//以Clear来实例化Calculus模板类(Calculus), Calculus类的所有对象都有与Clear类相同的返回类型.但是下面给出的Confused类:
struct Confused {
double operator()(int) const; // 参数类型
int operator()(double) const; // 不同于返回类型
};
//试图实例化Calculus会导致Calculus与Confused的返回类型不同.编译会产生一条从int转换到double的警告和一条从double转换到int的警告信息.
//在TR1引入,C++11也接受了模板类std::result_of,它允许我们在所有的声明中确定和使用函数对象的返回类型.下面的类CalculusVer2用std::result_of对象来获得函数对象的返回类型:
template< class Obj >
class CalculusVer2 {
public:
template<class Arg>
typename std::result_of::type operator()(Arg& a) const {
return member(a);
}
private:
Obj member;
};
//这种实例化CalculusVer2函数对象的方法没有类型转换,没有警告也没错误.
模板 std::result_of 在 TR1 和 C++11 的实现中有一点不同。TR1 的版本允许实现在特殊情况下,可能无法决定一个函数调用其回返值的类别。然而,因为 C++11支持了decltype,实现被要求在所有情况下,皆能计算出回返值类型。
7.12 原本计划加入但没有加入C++11的特性
预计由 Technical Report 提供支持的:
延后讨论的:
7.13 被移除或被废弃的特性