C++11新特性C++11标准发布已有一段时间了, 维基百科上有对C++11新标准的变化和C++11新特性介绍的文章. 我是一名C++程序员,非常想了解一下C++11. 英文版的维基百科看起来非常费劲,而中文版维基百科不是知道是台湾还是香港人翻译的然后由工具转换成简体中文的,有些术语和语言习惯和大陆程序不一样! 我决定结合这两个版本按照我自己的习惯把这篇文章整理到我的博客中.分享给关注我和关注C++11的朋友们. 当然了, 本人水平有限,英语水平也很一般,就把这个过程当做学习C++11的过程吧.文章中肯定会有很多错误或描述不恰当的地方. 非常希望看到的朋友能给我指出来. 以下是关于C++11的英文版本和中文版本维基百科的链接: 目录
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++ 委员会网站(英文)。
C++的修订范围包括核心语言以及标准程序库。在开发2011版标准的各个特性的过程中,标准委员会坚持以下指导思想: 对初学者的关注是很重要的,因为他们构成了计算机程序员的主体。也因为许多初学者不愿扩展他们的C++知识,他们仅限于掌握C++中自己所专精的部分.
C++标准委员会的一个职责是开发语言核心. 核心语言被大幅改进的领域包括: 多线程支持, 泛型编程支持, 统一的初始化和提高性能.
3.提高核心语言运行性能 3.1 右值引用和移动构造 这个操作不需要数组的复制,而且空的临时对象的析构也不会销毁内存。返回vector临时对象的函数只需要返回std::vector<T>&&。如果vector没有move 构造函数,那么就会调用常规拷贝构造函数。如果有,那么就会优先调用move构造函数,这能够避免大量的内存分配和内存拷贝操作。 右值引用不用对标准库之外的代码做任何改动就可以为已有代码带来性能上的提升. 返回值类型为std::vector<T>的函数返回了一个std::vector<T>类型的临时对象,为了使用移动构造不需要显示地将返回值类型改为std::vector<T>&&, 因为这样的临时对象会被自动当作右值引用. 但是在c++03中, std::vector<T>没有移动构造函数, 带有const std::vector<T>& 参数的拷贝构造会被调用, 这会导致大量内存分配和拷贝动作. 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()是常量.因为从理论上讲, 这个函数可以影响(改变)一个全局变量或调用其它非运行时常量函数等. constexpr int get_five() {return 5;} int some_value[get_five() + 7]; // 创建一个包含12个整数的数组. 这种形式在C++11中是合法的. 这样可以让编译器理解并验证get_five()是一个编译期常量!作用在函数上的constexpr关键字对函数的行为施加了一些限制. 首先, 这个函数的返回值类型不能是void; 其次, 在函数体中不能声明变量或新类型; 第三, 函数体内只能包含声明语句,空语句和单个return语句且,return语句中的表达式也必须是常量表达式. 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.1 外部模板 在C++03中,只要在编译单元内遇到被完整定义的模板,编译器都必须将其实例化(instantiate)。这会大大增加编译时间,特别是模板在许多编译单元内使用相同的参数实例化。没有办法告诉C++不要引发模板的实例化.C++11引入了外部模板声明, 就像外部数据声明一样. C++03用下面的语法迫使编译器实例化一个模板: template class std::vector<MyClass>; extern template class std::vector<MyClass>; 这些特性存在的主要目的是为了让C++更使用. 这些特性可以改进类型安全, 最小化代码重复, 尽可能减少错误代码等. C++03从C语言继承了初始化列表这一特性. 在一对大括号中列出参数的方式来给出一个结构体或者数组, 这些参数值按照各个成员在结构体中的定义顺序来排列.这些初始化列表是递归的,所以一个结构体数组或包含另一个结构体的结构体可以使用它们. struct Object { float first; int second; }; Object scalar = {0.43f, 10}; //One Object, with first=0.43f and second=10 Object anArray[] = {{13.4f, 3}, {43.28f, 29}, {5.934f, 17}}; //An array of three Objects 这对于静态初列表或者只想把结构体初始化为某个特定值而言是非常有用的. C++提供了构造函数来初始化对象, 但这没有初始化列表方便.C++03只允许符合POD定义的类型使用初始化列表,非POD的类型不能使用,就连相当有用的STL容器std::vector也不行.C++11扩展了初始化列表, 使用它可以用在所有类上,包括像vector这样的标准容器. class SequenceClass { public: SequenceClass(std::initializer_list list); }; 这使得可以从一串整数来创建SequenceClass对象, 例如: void FunctionName(std::initializer_list list); FunctionName({1.0f, -3.45f, -0.4f}); 标准容器也能够以这种方式初始化: std::vector v = { "xyzzy", "plugh", "abracadabra" }; std::vector v({ "xyzzy", "plugh", "abracadabra" }); std::vector v{ "xyzzy", "plugh", "abracadabra" }; // 参见下面 "统一的初始化"
C+03在初始化类型方面有着许多问题.初始化类型有数种方法,而且交换使用时不会都产生相同结果。传统的建构式语法,看起来像是函数声明,而且必须采取一些步骤保证不破坏编译器那些最让人恼火的解析规则.只有聚合体和POD类型能够用集合式初始化(通过SomeType var = {}; 形式的语法) struct BasicStruct { int x; double y; }; struct AltStruct { AltStruct(int x, double y) : x_{x}, y_{y} {} private: int x_; double y_; }; BasicStruct var1{5, 3.2}; AltStruct var2{2, 4.3}; var1初始化行为就像聚合初始化一样.也就是说,每个数据成员就是一个对对象, 按顺序从初始化列表中拷贝一个对应的值来初始化它们.如果有需要, 会进行隐式类型转换.如果存在向下类型转换(转换后的数据类型不能表示原数据类型,转换后可能有数据丢失,例如将unsigned转换成int), 那么这个程序就是病态的,会导致编译失败. var2的初始化则是简单地调用构造函数. struct IdString { std::string name; int identifier; }; IdString get_string() { return {"foo", 42}; //注意,这里没有指定具体类型. } 统一初始化不会取代构造函数语法,还是有一些时候是需要构造函数语法的.如果一个类有初始化列表构造函数(TypeName(initializer_list);),假定它有资格成为构造函数之一(我们知道,一个类可以有多个构造函数),那么它的优先级会高于其它形式的构造函数.C++11版本的std::vector就有一个初始化列表构造函数.这意味着 std::vector the_vec{4};会调用初始化列表构造函数,而不是调用以vector大小为唯一参数的构造函数. 要访问后一个构造函数, 用户必须直接使用标准构造函数语法. 在C++03(还有C)中,必须显式指定变量的类型.然而,随着模板类型和模板元编程技术的出现,某些东西的类型,尤其是函数的返回类型,可能不是那么容易表示的了. 在这种情况下,将中间结果存储在某个变量中是件很困难的事情.可能需要去了解特定的模板元编程库的内部实现. auto some_strange_callable_type = boost::bind(&some_function, _2, _1, some_object); auto other_variable = 5; some_strange_callable_type的类型很简单, 就是boost::bind模板函数返回值的类型.作为编译器语义分析责任的一部份,编译很容易确定这个类型,但程序员就没那么容易确定了.otherVariable 的类型同样也是定义明确的,程序员很容易就能判别。它是个int(整数),就和整数字面值的类型一样。 另外,关键字decltype可以用来在编译期确定表达式的类型.例如: int some_int; decltype(some_int) other_integer_variable = 5;
decltype 和 auto 一起使用会更为有用,因为 auto 参数的类型只有编译器知道.然而 decltype对于那些大量运用运算符重载和类型特化来编码的表达式非常有用。auto对减少代码冗余也很有用.比如说, 程序员不用像下面这样写代码: for (std::vector::const_iterator itr = myvec.cbegin(); itr != myvec.cend(); ++itr) //而可以用更简短的形式: for (auto itr = myvec.cbegin(); itr != myvec.cend(); ++itr) 这两种形式的差距会随着你使用的容器的嵌套层次而增加, 这种情况下typedef也是一种减少代码的好方法!由decltype得出的类型可以和由auto推导出的类型不同: #include<vector> int main() { const std::vector v(1); auto a = v[0]; // a 是 int 类型 decltype(v[1]) b = 1; // b 是 const int& 类型, 是std::vector::operator[](size_type) const // 的返回类型 auto c = 0; // c 是 int 类型 auto d = c; // d 是 int 类型 decltype(c) e; // e 是 int 类型, c变量的类型 decltype((c)) f = c; // f 是int&类型, 因为(c)是一个左值 decltype(0) g; // g 是 int 类型, 因为0是一个右值 } 在C++03中,要遍历一个list中的元素需要很多代码.其它语言实现支持"糖块句法",允许程序通过一个简单的"foreach"语句自动遍历list中的元素.其中之一就是Java语言, 它从5.0开始支持增强的for循环. int my_array[5] = {1, 2, 3, 4, 5}; // double the value of each element in my_array: for (int &x : my_array) { x *= 2; } 这种形式的for语句叫作"基于范围的for语句",它会遍历列表中的每一个元素.可以用在C风格数组,初始化列表和那些带有能返回迭代器的begin()和end()函数的类型上.所有提供了begin/end的标准容器都可以使用基于范围的for语句. C++11提供了创建匿名函数的能力,叫做Lamda函数. 具体内容请参考: http://www.cnblogs.com/pzhfei/archive/2013/01/14/lambda_expression.html 标准C的函数声明语法对C语言的特性集而言完全足够了. 因为C++从C发展而来, 保留了C的基本语法并在需要的地方进行了扩展. 然而,C++的结构变得更加复杂了,暴露出了很多的局限性,尤其是模板函数的声明.下面的例子在C++03中是不允许的: template Ret adding_func(const Lhs &lhs, const Rhs &rhs) {return lhs + rhs;} //Ret must be the type of lhs+rhs Ret的类型是lhs+rhs的结果的类型.就算用前面提到的C++11中的decltype,也是不行的: template decltype(lhs+rhs) adding_func(const Lhs &lhs, const Rhs &rhs) {return lhs + rhs;} //Not legal C++11 这不是合法的C++,因为lhs和rhs还没定义;解析器解析完函数原型的剩余部分之前,它们还不是有效的标识符. template auto adding_func(const Lhs &lhs, const Rhs &rhs) -> decltype(lhs+rhs) {return lhs + rhs;} 这种语法可以用到更普通的函数声明和定义上: struct SomeStruct { auto func_name(int x, int y) -> int; }; auto SomeStruct::func_name(int x, int y) -> int { return x + y; } 关键字auto的这种用法与在自动类型推导中有所不同. C++03中类的构造函数不允许调用该类的其它构造函数;每个构造函数都必须自己或者调用一个公共的成员函数来构造类的全部成员.例如: class SomeType { int number; public: SomeType(int new_number) : number(new_number) {} SomeType() : number(42) {} }; class SomeType { int number; private: void Construct(int new_number) { number = new_number; } public: SomeType(int new_number) { Construct(new_number); } SomeType() { Construct(42); } }; 而且,基类的构造函数不能直接暴露给派生类;每个派生类必须实现自己的构造函数哪怕基类的构造函数已经够用了.非静态数据成员不能在声明的地方初始化.它们只能在构造函数中初始化. C++11为这些问题提供了解决方案.C++11允许构造函数调用另一个构造函数(叫做委托构造).这允许构造函数利用其它构造函数的行为而只需增加少量的代码.C#,java和D语言都提供了这种功能. C++的语法如下: class SomeType { int number; public: SomeType(int new_number) : number(new_number) {} SomeType() : SomeType(42) {} }; 注意:这个例子可以通过给new_number设定一个默认参数来达到相同的效果.但是,这种新语法可以让这个默认值在实现中来设置而不是在接口中设置.这带来的一个好处就是,对库代码的维护者而言,在接口中(头文件中)声明默认值,这个默认值被嵌入到了调用端;要改变这个默认值的话,调用端的代码都需要重新编译.但委托构造可以在实现中(CPP文件中)来改变这个默认值, 这样调用端的代码就不需要重新编译,只用重新编译这个库就可以了. class BaseClass { public: BaseClass(int value); }; class DerivedClass : public BaseClass { public: using BaseClass::BaseClass; }; 对于成员初始化,C++11允许下面这样的语法: class SomeClass { public: SomeClass() {} explicit SomeClass(int new_value) : value(new_value) {} private: int value = 5; }; 每一个构造函数都将把value初始化为5, 如果它们没用其它值来覆盖这个初始化的话.上面那个空的构造函数会把value初始化为类定义时的状态5.而那带有参数的构造函数会用指定的值来初始化value.成员的初始化也可以使用前面提到的统一初始化. 在C++03中,很容易让你在本想重写基类某个函数的时候却意外地创建了另一个虚函数.例如: struct Base { virtual void some_func(float); }; struct Derived : Base { virtual void some_func(int); }; 本来Derived::some_func函数是想替代Base中那个函数的.但是因它的接口不同, 又创建了一个虚函数.这是个常见的问题, 特别是当用户想要修改基类的时候. struct Base { virtual void some_func(float); }; struct Derived : Base { virtual void some_func(int) override; // 病态的,不会重写基类的方法 }; override 这个特殊的标识符意味编译器将去检查基类中有没有一个具有相同签名的虚函数,如果没有,编译器就会报错! struct Base1 final { }; struct Derived1 : Base1 { }; // 病态的, 因为类Base1被标记为final了 struct Base2 { virtual void f() final; }; struct Derived2 : Base2 { void f(); // 病态的, 因为虚函数Base2::f 被标记为final了. }; 在这个例子中, virtual void f() final;语句声明了一个虚函数却也阻止了子类重写这个函数.它还有一个作用,就是防止了子类将那个特殊的函数名与新的参数组合在一起. 本节中出的"0"都将解释为"一个求值结果为0的int型常量表达式". 实际上任何整数类型都可以作为常量表达式. void foo(char *); void foo(int); 如果NULL定义为0,那么foo(NULL);语句将会调用foo(int).这几乎必定不是程序员想要的,也不是代码直观上要表达的意图. char *pc = nullptr; // OK int *pi = nullptr; // OK bool b = nullptr; // OK. b is false. int i = nullptr; // error foo(nullptr); // calls foo(char *), not foo(int);
在C++03中,枚举不是类型安全的.他们实际上是整数,尽管他们是不同的枚举类型.这使得我们可以比较两种不同类型的枚举值.C++03提供的唯一安全性就是,一个整数或一个枚举类型的值不能隐式地转换成另一个枚举类型.此外,底层的具体的整数类型(short,long,int,...)是由实现(编译器)定义的,标准并无明确规定.因此,那些枚举变量的大小的代码将是不可移植的.最后,枚举值是暴露在外层作用域(直接包含枚举定义的作用域)中的.所以,两个不同枚举类型的成员不可能有相同的名字. enum class Enumeration { Val1, Val2, Val3 = 100, Val4 // = 101 }; 这种枚举是类型安全的;枚举值不能隐式地转换成整数,所以也不可以和整数做比较.表达式 Enumeration::Val4 == 101会报一个编译错误. enum class Enum2 : unsigned int {Val1, Val2}; 老式的枚举被放在直接包含该定义的作用域中.新式的枚举被放在枚举类的作用中.所以,上例中Val1是未定义的,而Enum2::Val1是已定义的. enum Enum3 : unsigned long {Val1 = 1, Val2}; 这个例子中枚举名字被定义在枚举类型的作用域内(Enum3::Val1),但是为了向下兼容它们也会被放在直接包含在Enum3所在的作用域中. C++03的解析器都把">>"定义为右移运算符.但是,在嵌套的模板声明中,程序员往往倾向于忽略两个右尖括号之间的空格.这会导致编译器报一个语法错误. template<bool Test> class SomeType; std::vector<SomeType<1>2>> x1; // 被解析成 std::vector of SomeType<true> 2>, // 这是错误的语法, 1 被当成 true 了. std::vector<SomeType<(1>2)>> x1; // 被解析成 std::vector of SomeType<false>, // 在C++11中是合法的. (1>2) 这个表达式的结果为false. C++98 增加了explicit关键字来防止单参数的构造函数被用作隐式的类型转换操作符.然而,却没有对真正的类型转换操作符这样做.例如,智能指针可能定义了operator bool(),让它的行为更像真原始指针.有了这个转换操作符就可以用if语句来测试:if (smart_ptr_variable),当这个指针不为空时结果为真,否则为假.但是,这也会引起其他一些非预期的转换操作.因C++中的bool被定义为一种算术类型,可以隐式地转换为整数甚至是浮点数进行数学运算. 拿转换出的布尔值进行非布尔计算的数学计算,往往不是程序员想要的. 在进入这个主题前,先弄清楚"模板"和"类型"的区别.类型,是具体的数据类型,可以直接用来定义变量. 模板,是类型的模板,根据这个模板可以产生具体的类型;模板是不能直接定义变量的;当指定了所有的模板参数后,就产生了一个具体的类型,就可以用来定义变量了. template <typename First, typename Second, int Third> class SomeType; template <typename Second> typedef SomeType<OtherType, Second, 5> TypedefName; // 在C++03中, 这是非法的. C++11增加为模板定义别名的能力,用下面这样的语法: template <typename First, typename Second, int Third> class SomeType; template <typename Second> using TypedefName = SomeType<OtherType, Second, 5>; 这种using语法也可以用来定义类型的别名: typedef void (*FunctionType)(double); // 老式语法 using FunctionType = void (*)(double); // 新式语法
C++03中,对哪些类型的对象能够作为联合的成员是有限制的.例如,联合不能包含定义了非平凡构造函数的对象.C++11废除了其中的一些限制: #include <new> // Required for placement 'new'. struct Point { Point() {} Point(int x, int y): x_(x), y_(y) {} int x_, y_; }; union U { int z; double w; Point p; // 非法的C++03; 合法的C++11. U() {new(&p) Point();} // 由于Point的原因, 必须定义构造函数. }; 因为是放宽了现有的规则,所以不会对已有的代码造成影响. 这些特性让C++语言可以完成那些以前不可能的,极其繁琐的或者需要一些不可移植的库才能完成的事情. 在 C++11 之前, 不论是类模板或是函数模板,都只能按其被声明时所指定的样子,接受一组数目固定的模板参数.C++11 加入新的表示法,允许任意个数,任意类型的模板参数,不必在定义时将参数的个数固定。 template<typename... Values> class tuple;
模板类 tuple 的对象,能接受不限个数的 typename 作为它的模板形参: class tuple<int, std::vector<int>, std::map<std::string, std::vector<int>>> someInstanceName; 实参的个数也可以是0,所以 class tuple<> someInstanceName 这样的定义也是可以的。 若不希望产生实参个数为 0 的变长参数模板,则可以采用以下的定义: template<typename First, typename... Rest> class tuple; 变长参数模板也能运用到函数模板上。传统 C 中的 printf 函数,虽然也能做到不定个数的形参来调用,但其并非类型安全的。以下的例子中,C++11 除了能定义类型安全的变长参数函数外,还能让类似 printf 的函数能自然地处理自定义类型的对象。 除了在模板参数中能使用...表示不定长模板参数外,函数参数也使用同样的表示法代表不定长参数。 template<typename... Params> void printf(const std::string &strFormat, Params... parameters); 其中,Params 与 parameters 分别代表模板与函数的变长参数集合,称之为参数包 (parameter pack).参数包必须要和运算符"..."搭配使用,避免语法上的歧义。 变长参数模板中,无法像在类或函数中那样使用参数包.因此典型的做法是以递归的方法取出可用参数,请看以下的 C++11 printf 例子: void printf(const char *s) { while (*s) { if (*s == '%' && *(++s) != '%') throw std::runtime_error("invalid format string: missing arguments"); std::cout << *s++; } } template<typename T, typename... Args> void printf(const char* s, T value, Args... args) { while (*s) { if (*s == '%' && *(++s) != '%') { std::cout << value; printf(*s ? ++s : s, args...); // 即便当 *s == 0 也会产生调用,以检测更多的类型参数。 return; } std::cout << *s++; } throw std::logic_error("extra arguments provided to printf"); } printf 会不断地递归调用自身:函数参数包 args... 在调用时, 会被模板类匹配分离为 T value和 Args... args.直到 args... 变为空参数,则会与简单的 printf(const char *s) 形成匹配,退出递归。 另一个例子为计算模板参数的个数,这里使用相似的技巧展开模板参数包 Args...: template<> struct count<> { static const int value = 0; }; template<typename T, typename... Args> struct count<T, Args...> { static const int value = 1 + count<Args...>::value; }; 虽然没有一个简洁的机制能够对变长参数模板中的值进行迭代,但使用运算符"..."还能在代码各处对参数包施以更复杂的展开操作。举例来说,一个模板类的定义如下: template <typename... BaseClasses> class ClassName : public BaseClasses... { public: ClassName (BaseClasses&&... baseClasses) : BaseClasses(baseClasses)... {} } BaseClasses... 会被展开成类型 ClassName 的基底类; ClassName 的构造函数需要所有基类的左值引用,而每一个基类都是以传入的参数做初始化 (BaseClasses(baseClasses)...)。 在函数模板中,变长参数可以和左值引用搭配,达成形参的完美转发 (perfect forwarding): template<typename TypeToConstruct> struct SharedPtrAllocator { template<typename... Args> std::shared_ptr<TypeToConstruct> ConstructWithSharedPtr(Args&&... params) { return tr1::shared_ptr<TypeToConstruct>(new TypeToConstruct(std::forward<Args>(params)...)); } }
参数包 parms 可展开为 TypeToConstruct 构造函数的形参。 表达式std::forward<Args>(params) 可将形参的类别信息保留(利用右值引用),传入构造函数。 而运算符"..."则能将前述的表达式套用到每一个参数包中的参数。这种工厂函数(factory function)的手法, 使用 std::shared_ptr 管理配置对象的内存,避免了不当使用所产生的内存泄漏(memory leaks)。 此外,变长参数的数量可以藉以下的语法得知: template<typename ...Args> struct SomeStruct { static const int size = sizeof...(Args); } SomeStruct<Type1, Type2>::size 是 2,而 SomeStruct<>::size 会是 0。 (sizeof...(Args) 的结果是编译期常数。) C++03提供两种字符串字面值.第一种,包含在一对双引号内,产生一个以空字符结尾的const char数组.第二种,由L""定义,产生以空字符结尾的const wchar_t类型的数组.wchar_t是一个大小和定义都未明确定义的宽字符.字符串字面值既不支持UTF-8,UTF-16也不支持其它任何类型的unicode编码. 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" C++03提供几种字面值.字符串"12.5"会被编译器解析为double类型的值12.5. 但是,带有'f'后缀的字符串"12.5f"会创建一个float类型的值12.5. 后缀修饰符已经被C++标准固定下来了, 用户代码不能增加新的修饰符! 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关键字和编译器能在编译期就完全转换这些字面值. 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.
内存模型允许编译器完成很重要的优化.即使像移动程序中的语句来合并循环这样简单的编译器优化都能够影响对潜在共享变量读,写操作的顺序!改变读写顺序会导致竞态条件的产生.没有内存模型,编译器一般不能将这种优化应用到多线程程序中的,或者只能用于某些特殊情况.现代程序设计语言,比如Java,为此实现了一个内存模型.内存模型指定了同步屏障(Synchronization Barriers),通过特殊的、定义好的同步操作(比如获得一个进入同步块或某方法的锁)来建立的.内存模型规定,共享变量值的改变只需要对那些通过了同步屏障的线程是可见的.此外,竞态条件这个概念的完整定义覆盖了带有内存屏障细节的操作顺序. 这些语义给了编译器更高的自由度去进行优化: 编译器只需要确保优化前和优化后同步屏障内的变量(可能被共享)的值是一样的. 在多线程环境中,线程通常都有一些自己所独有的变量. 函数的局部变量也是这样, 但是全局变量和静态变量就不一样了. C++03中,如果类没有定义构造函数,拷贝构造函数,赋值函数和析构函数的话,编译器会为类提供这些函数.程序员可以自己定义这些函数来覆盖编译生成的默认版本.C++还定义了几个可以作用在所有类上的操作符(比如,赋值操作符=,new操作符等),程序员也可以覆盖它们. 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; //不能调用这个函数 };
C++03中,最大的整数类型是long int.它保证使用的位数至少与int一样. 这导致long int在一些实现是64位的, 而在另一些实现上却是32位的.C++11增加了一个新的整数类型long long int来弥补这个缺陷.它保证至少与long int一样大,并且不少于64位.这个类型早在C99就引入到了标准C中, 而且大多数C++编译器都以扩展的形式支持这种类型了. C++03提供两种方法来测试断言:宏assert和#error预处理指令.然而,这不适合用在模板中:宏在运行期间测试断言,而预处理指令在编译预处理阶段测试断言,这些都发生在模板实例化之前;也不适合用于依赖于模板参数的属性. 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<Integral>::value, "foo() parameter must be an integral type."); } 当常量表达的结果为false时,编译器就会产生一个错误消息.第一个例子类似于预处理指令#error,但是预处理指令只支持整数类型.相比之下,第二例子中的断言在模板类Check每一次被实例化的时候都被检查一次. 6.9 允许sizeof运算符作用在类型的数据成员上,无须明确的对象 C++03中,sizeof可以作用在类和对象上.但却不能像下面这样做: struct SomeType { OtherType member; }; sizeof(SomeType::member); // C++03 不行. C++11 可以. //这会返回OtherType的大小.C++03不允许这样做,会报一个编译错误.C++11允许这样做.
C++11可以用alignof和alingas来查询和控制变量的对齐方式. alignof是一个操作符,他以一个类型为参数,并且返回这个类型的实例必须分配的字节边界值,这个值一定是2的整数次幂.如果参数是引用类型,那么返回的是被引用的类型的对齐信息.对于数组,返回的是元素类型的对齐信息. 之前版本的C++标准通过set_new_handler提供了程序员驱动的垃圾回收机制,但却没有为自动化垃圾回收机制给出对象可到达性的定义. C++11定义了指针完全地从其他地方获得值的条件.编译器实现可以指定在严格的指针安全下进行操作,在这种情况下不按这个规则获得值的指针就会变成无效的. 6.12 属性 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标准库引入了很多新特性.很多是在旧标准下实现的,但是有一些却依赖于C++11的核心特性.新标准库的大部分是在2005年公布的C++标准委员会标准库技术报告(tr1)中定义的.各种完全或部分的TR1实现在现行的标准中可以通过命名空间std::tr1来引用了.对于C++11,这些实现被移到了命名空间std中.然而,因为TR1的特性被引入到了C++11的标准库中,所以需要更新它们以适合那些在最初的TR1中不可用的C++11特性. C++11提供了很多现存标准库组件能从中获益的新特性.例如,大多数标准容器都可以从基于移动构造右值引用中获益,不管是快速移动重型容器还是把容器的内容移动到新的内存位置.标准库组件已经用适当的C++11新特性升级过了.包括但不限于以下特性:
C++11虽然从语言上提供了支持线程的内存模型,但主要的支持还是来自标准库. 元组(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!"); 可以在定义一个元组时不定义他的内容,不过只有当它的每个元素类型都有默认构造函数时才可以这样做.而且,可以将一个元组赋值给另一个元组:如果两个元组的类型相同,且每种元素类型都有拷贝构造函数;否则,需要右边的元组的元素可以隐式转换成左边元组的对应元素类型或者左边元组的元素类型有合适的构造函数. 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<T>::value //返回元组 T 内的元素个数, std::tuple_element<I, T>::type //返回元组 T 内的第 I 个元素的类型
在过去,不断有要求想将散列表(无序关系式容器)引进标准库。只因为时间上的限制,散列表才没有被标准库所采纳。虽然,散列表在最糟情况下(如果出现许多冲突 (collision) 的话)在性能上比不过平衡树。但实际运用中,散列表的表现则较好。
这些类完全具备容器类需的条件,同时也提供访问其中元素的成员函数: insert, erase, begin, end。 新的标准库定义了一个新的头文件<regex>,由一些新的类组成:
函数 regex_search 是用来搜索模式的; 若要搜索并替换,则要使用函数 regex_replace,该函数会返回一个新的字符串。算法regex_search 和 regex_replace 接受一个正则表达式(模式)和一个字符串,并将该模式匹配的结果情况存储在 struct 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)可以用来避免这一问题。库 <regex> 不需要改动到现有的头文件,同时也不需要扩展现有的语言特性。 这些指针是由 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 提出的那些需要移动语义的容器之中。 C 标准库允许使用rand函数来生成伪随机数。不过其算法则取决于各程序库开发者。 C++ 直接从 C 继承了这部份,但是 C++11 将会提供产生伪乱数的新方法。C++11 的随机数功能分为两部分: 第一,一个随机数生成引擎,其中包含该生成引擎的状态,用来产生随机数。第二,一个分布,这可以用来决定产生随机数的范围,也可以决定以何种分布方式产生随机数。随机数生成对象即是由随机数生成引擎和分布所构成。 不同于 C 标准库的 rand; 针对产生随机数的机制,C++11 将会提供三种算法,每一种算法都有其强项和弱项:
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(); // 产生随机数 我们可以通过实例化模板类 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<void (int &r), int>' // 'i' 不会被修改 std::cout << i << std::endl; // 输出 0 g (f, std::ref(i)); // 实例化 'g<void(int &r),reference_wrapper<int>>' // 'i' 会被修改 std::cout << i << std::endl; // 输出 1 } 这项功能将加入头文件 <utility> 之中,而非通过扩展语言来得到. 函数对象的多态包装器(又称多态函数对象包装器)在语义和语法上和函数指针相似,但不像函数指针那么狭隘。只要能被调用,且其参数能与包装器兼容的都能以称之为多态函数对象包装器(函数指针,成员函数指针或仿函数)。 通过以下例子,我们可以了解多态函数对象包装器的特性: std::function<int (int, int)> func; // 利用模板类 'function' // 建立包裝器 std::plus<int> add; // 'plus' 被声明 'template<class T> 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 将定义在头文件 <functional>,而不须改动语言本身。 编写一个创建或修改其它程序(也可以是程序本身)的程序称为元编程.这种行为可以发生在编译期,也可以发生在运行期.C++标准委员会决定引入一个库,允许在编译期利用模板进行元编程. template<int B, int N> struct Pow { // 递归调用和重新组合. enum{ value = B*Pow<B, N-1>::value }; }; template< int B > struct Pow<B, 0> { // ''N == 0'' 是终止条件. enum{ value = 1 }; }; int quartic_of_three = Pow<3, 4>::value; 很多算法都可以操作不同类型的数据;C++的模板支持泛型编程,并且使代码更加紧凑和有用.然而算法通常都需要知道它正使用的数据的类型.这些信息可以在编译期利用类型特征(type trait)获取到. 类型特征(type traits)可以标识出一个对象的类别和类或结构体的全部特征.type traits定义在一个新头文件<type_traits>中. // 算法一 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<std::is_integral<T1>::value && std::is_floating_point<T2>::value>::do_it( A, B ) ; } 通过定义在<type_transform>头文件中的类型特征,可以创建类型转换操作(static_cast和const_cast对模板来说是不够的).这种编程技术能产生优雅简洁的代码,但是除错却是这种技术的弱点:编译期的错误信息难以理解,运行期错误则更难. 要在编译期确定一个模板函数的返回值类型不是那么容易的,特别是当返回类型依赖于函数参数的时候.例如: 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<Clear>), Calculus类的所有对象都有与Clear类相同的返回类型.但是下面给出的Confused类: struct Confused { double operator()(int) const; // 参数类型 int operator()(double) const; // 不同于返回类型 }; //试图实例化Calculus<Confused>会导致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<Obj(Arg)>::type operator()(Arg& a) const { return member(a); } private: Obj member; }; //这种实例化CalculusVer2<Confused>函数对象的方法没有类型转换,没有警告也没错误.
模板 std::result_of 在 TR1 和 C++11 的实现中有一点不同。TR1 的版本允许实现在特殊情况下,可能无法决定一个函数调用其回返值的类别。然而,因为 C++11支持了decltype,实现被要求在所有情况下,皆能计算出回返值类型。
预计由 Technical Report 提供支持的:
延后讨论的:
|
|