C++0x(C++11)新特性点评

白杨

baiy.cn

 

C++11 在不久之前已获通过,它对 C++03 的扩充主要体现在核心语言和标准库两个方面。作为《C++编码规范与指导》中的一个小节,本文主要讨论 C++11 中,新特性所带来的变化。


目录

 

返回目录


核心语言

右值引用和移动构造语义

说明

右值引用(Rvalue reference,T&&)解决了了左值引用(T&)无法传递临时对象和常引用 (const T&)传递的对象只读的问题。右值引用允许传递一个可变更的临时对象引用,主要用于配合移动构造。

移动构造(move constructor)使用移动而非复制语义完成构造过程,主要用于解决从函数返回值时的大量深拷贝开销。使用右值引用和移动构造,在按值传递和返回临时对象时,即可免去不必要的内存分配和数据拷贝开销。

优点

  • 在特定情况下,可实现内存零拷贝,从而大大提高执行效率。

缺点

  • 进一步复杂化了对象传递的语义。可想而知,复合使用了传址(指针、引用、右值引用,和它们的各种常量变体)以及传值等各种对象传递方法的代码,其学习和维护成本都将较高。并且无论对其使用者还是实现者来说,都比较容易产生 bug。
  • 需要为每个对象考虑多实现一种新的构造方法。
  • 在传统的 C++03 代码中,通过智能指针等手段完全可以实现同样的零拷贝。若使用了带有引用计数/写时拷贝等特性的代理实现,还能够达到更好的零拷贝效果和更优的性能。
  • 通过智能指针等手段达到零拷贝的目的,其方便性和易用性至少能够与右值引用+移动构造相当。

总结

只有在实现标准库等某些必须返回值(而不能返回智能指针等句柄对象,也不能返回引用)的场合,才考虑使用右值引用+移动构造方案。其它情况下,智能指针或代理类通常是更好的选择。

 

常量表达式(constexpr)

说明

constexpr 主要用于修饰函数返回值,表明该函数的返回值是编译时已知的,意即:此函数内仅包含一条形如“return 常量表达式;”这样的语句。constexpr 同样也可以用来修饰对象,如果用它来修饰用户自定义类型,则用户需要为该类型配备 constexpr 构造函数,该构造函数同样仅允许使用编译时已知的常量完成成员初始化。

优点

  • 比内联还让函数像宏。

缺点

  • 还不如宏能做的事情多。

总结

意思不大。

 

放宽的 POD 类型定义

说明

POD(Plain Old Data)类型指那些布局与 C struct 兼容的用户自定义类型。C++03 中对 POD 类型的定义过于严苛,在新标准中,基本上,只要一个类不使用虚表和虚基类;只使用默认的构造和析构函数,并且其所有基类和成员也都如此的话,这个类的布局就是 POD 兼容的。新标准取消了所有成员都必须是 public 访问权限等没有意义的限制条件。

优点

  • 无。

缺点

  • 无。

总结

事实上,貌似从 C++ 诞生开始,广大编译器厂商就一直在使用上述放松标准了。因此这一改动只不过是顺应事实之举,对现有 C++ 程序员来说没有任何影响。

 

extern 模板修饰

说明

通过使用 extern 来修饰一个模板类,告知编译器该模板无需在当前编译单元内实例化(必要的实例化已在其它编译单元内进行)。

优点

  • 提供对于程序员来说,更明确,与类型实例化(外部变量和对象定义)看上去更相似的模板显式实例化方式。

缺点

  • 无。

总结

大部分近代 C++ 编译器都已支持模板的显式实例化。甚至不少编译也早已允许在模板显式实例化语句前面加个“extern”修饰词了。因此跟上一条一样,这也是属于“先上车后补票”的勾当。

 

初始化列表

说明

允许非 POD 容器类型通过初始化列表完成构造。

优点

  • 对于很多容器类型来说(比如:std:vector),可大大方便其创建过程。

缺点

  • 需要添加额外的构造函数。

总结

好用的功能,很适合经常需要通过初始化列表来构造容器对象的场合。

 

统一的初始化方式

说明

无论是 POD 还是非 POD 类型都可以使用 obj = { ... } 的方式来进行初始化。对于非 POD 类型 { ... } 将自动匹配和调用构造函数。因此,构造函数 T(x, y) 现在也可以写成:T{x, y}、T var{x, y} 等。在需要返回一个 T 类型对象时,甚至可以直接写:“return {x, y};”

优点

  • 一致的构造方式便于编译器和辅助工具完成语法解析。
  • 可以灵活地构造和创建对象。

缺点

  • 滥用类似“return {x, y};”形式的自动类型推断很容易降低代码易读性和可维护性。

总结

分场合酌情使用。

 

类型推断

说明

在 C++11 中,auto 关键字的语义有所更改,从原来的声明一个存活于当前调用栈上的自动变量,转变成了通过表达式的右值来自动推断左值的类型。decltype 关键字则用于判断指定表达式的类型。

优点

  • 在定义模板,或需要获得 Lambda 表达式等匿名对象的类型时,可提供方便。

缺点

  • 在模板中,应只在类型参数未知或很难获得的时候才应考虑使用上述机制。否则很容易极大地降低代码易读性和可维护性。
  • 类似地,在普通代码中,只应在操作匿名类型对象时,才使用上述机制。
  • 虽然在 C++03 中,auto 关键字很少用到。但也不可避免的带来了兼容性问题——很少用到毕竟不等于完全没用到。

总结

应仅在“被逼无奈”时使用。

 

基于范围的遍历操作

说明

没啥好多说的,就是现如今已快烂大街的 foreach 类操作了。对一些特殊容器和大部分守规矩(支持迭代子和 begin()、end() 调用)的容器类型都有效。

优点

  • 方便完成容器遍历。

缺点

  • 无。

总结

随大流的 for 语句便捷写法。没有它日子也能过,有了确实会方便些,但也程度有限。

 

Lambda 表达式和闭包

说明

可在表达式中定义一个匿名函数对象,并返回它的实例。对闭包(closure)的简单解释:一段可调用的代码加上它执行时所依赖的上下文。在 C++11 中,闭包也是通过匿名函数对象的自动生成来实现的,例子:
std::vector<int> some_list;
int total = 0;
int value = 5;
std::for_each(some_list.begin(), some_list.end(), [&, value, this](int x) {
  total += x * value * this->some_func();
});

相当于:

class FOUnnamed {
private:
  int&  m_rnTotal;
  int   m_nValue;
  CEnv* m_pEnv;

public:
  FOUnnamed(int& total, int value, CEnv* this)
    : m_rnTotal(total), m_nValue(value), m_pEnv(this)
    {}

  void operator()(int x)
  {
    m_rnTotal += x * m_nValue * m_pEnv->some_func();
  } 
};

std::vector<int> some_list;
int total = 0;
int value = 5;
FOUnnamed foPred(total, value, this);
std::for_each(some_list.begin(), some_list.end(), foPred);

因此可以认为 lambda 表达式和闭包是一种生成匿名函数对象的快捷方式。

优点

  • 大大简化了函数对象的生成,非常适合于用作 STL 算法的谓词。由于 STL 算法中的大部分都是类似 std::for_each 这种的简单算法,如果没有 Lambda 表达式和闭包的话,如上例所示,为其定义谓词的代价过于高昂,以至于直接使用迭代子和 for 语句完成遍历反而更便捷。
     
  • 可以直接编写某些简单运算的函数对象临时实例,例如:“auto foFunc = [](int x, int y) -> int { int z = x + y; return z; }”,使得函数对象更便于使用。
     
  • 由于 lambda 表达式生成一个无名类型的实例,因此即使在头文件的内联函数中也可以使用函数对象,不用担心将该文件 include 到多个编译单元时发生编译错误。
     

  • 使用闭包的描述方式更容易被编译器优化。

缺点

  • 仅适用于封装只有几行代码的简单函数对象。如果滥用则很容易极大地降低代码易读性和可维护性。

总结

经典的特性,如果使用恰当,能发挥强大的威力。

 

返回值后置式函数声明语法

说明

类似 lambda 表达式中的返回值定义方式,模板和普通函数也可以使用类似的语法指定返回值,例如:

template<class Lhs, class Rhs>
  auto adding_func(const Lhs &lhs, const Rhs &rhs) -> decltype(lhs+rhs) {return lhs + rhs;}

显而易见,这种语法主要用来帮助完成模板定义时的类型推断——在上例中,由于用来推断返回类型的“lhs”和“rhs”在“adding_func(...)”之前还未定义,因此必须将返回值类型定义延迟到参数列表之后。

应注意到,使用这种后置语法时,函数原来指定返回值类型的位置必须记做“auto”。

优点

  • 方便一些特殊情况下的返回值定义。

缺点

  • 如果滥用则容易降低代码易读性和可维护性。

总结

要么在整个项目中一致地使用,要么仅作为一种解决特定问题的手段,在“被逼无奈”时才使用。

 

委托和成员默认初始值

说明

在 C++11 中,一个构造函数可以调用该类中的其它构造函数来完成部分初始化任务(委托)。声明成员时可以直接指定默认初始值,例如:

class CSomeType {
  int m_nNumber;
  int m_nValue = 10;

public:
  CSomeType(int new_number) : m_nNumber(new_number) {}
  CSomeType() : CSomeType(42) {}
};

优点

  • 可在一定程度上简化初始化相关代码。
  • 比起将构造的公共部分封装为一个私有方法供所有构造函数调用,委托更便于编译器实现一些优化。

缺点

  • 在 C++03 中,如果多个构造函数需要使用同一段初始化代码,亦可将这段代码封装为一个私有方法供所有构造方法调用。
  • 委托的加入改变了构造的语义:C++03 中,当一个对象的构造函数返回时,该对象构造完成;而在新语义中,仅当一个对象的所有构造函数调用都返回后,才表明该对象构造完成,使得对象构造过程被进一步地复杂化。
  • 类似地,基类和成员的构造语义也被相应地复杂化:仅当基类中的所有构造函数调用均返回时,派生类成员才开始构造;当成员中的所有构造函数均返回后,派生类才开始构造……

总结

利大于弊,值得一试。但要搞清楚新的语义,还要考虑好构造函数中的异常抛出与处理方式。

 

override 和 final 修饰符

说明

override 修饰符用于标识指定的虚方法重载了基类中的同名虚方法——而不是定义一个新的同名虚方法(比如仅参数列表不同的虚方法)。该修饰符导致编译时的严格检查,避免因为函数签名不同而重载失败。

final 修饰符可用于修饰类或方法。在修饰类时,它表示指定类是一个 concrete 类——不能再作为基类而被其它类继承。

将 final 修饰符作用于方法时,表示此虚方法已经是最终实现,任何在派生类中重载这个方法的企图都将引发一个编译错误。

优点

  • 定义和派生抽象类的利器,可在编译时发现诸如:修改了基类中某虚方法的参数列表后,忘记在其派生类中做出相应修改等各类相关错误。
  • 使得类和虚函数定义的语义更清晰,有利于增强可读性。
  • 为编译器提供了更多的优化线索。
  • override 和 final 只是修饰符,不是关键字。它们除了在特定上下文中有特别含义外,仍然可以作为合法的标识符使用。

缺点

  • 无。

总结

居家旅行,外出打鸟的必备佳品 :-)

 

NULL 指针常量

说明

C++11 中定义了语言级的 NULL 指针常量:nullptr。

优点

  • 比起我们自定的“NULL”宏来说,nullptr 常量更利于函数重载。

缺点

  • 无。

总结

无论是 NULL 还是 nullptr,都应尽可能一致地使用(尽量避免混用)。

 

强类型枚举

说明

C++11 通过“enum class ...”的语法为枚举提供了强类型支持,同时还允许用户指定枚举的具体类型,如:16位无符号整形;32位整形;64位整形等等。

优点

  • 强类型检查杜绝了不同枚举类型之间的比较或枚举与整形之间的比较,能够在编译时发现更多错误。
  • 可指定枚举的位宽大大方便了很多原来不方便使用枚举的场合(例如:可以打破 32 位限制、可以显式强制保证 8 位尺寸等等)。

缺点

  • 无。

总结

推荐使用。

 

改进的大于号解析

说明

定义模板实例时,不再需要小心地在多个连续的大于号之间添加空格了。

优点

  • 不必再担心模板定义中的“>>”被解析为右移操作。

缺点

  • 无。

总结

早就该有的特性,这算是语言级的 bugfix?

 

显式类型转换操作

说明

在 C++11 中,explicit 修饰符已可应用于类型转换操作,帮助编译器更好地完成类型转换和函数重载判定。

优点

  • 一个与转换操作的类型严格匹配的场合可以自动应用显式类型转换,这使得显式类型转换操作可以在不降低易用性的前提下,提供更好的类型匹配。

缺点

  • 无。

总结

基本上也属于是早就该有的特性,相信很多程序员都试着写过“explicit operator bool()”、“explicit operator INT64()”之类的东东。

 

模板别名

说明

可以使用 using 语法来定义模板实例的别名。效果跟 typedef 基本一样。

优点

  • 无。

缺点

  • 官方的说法是 C++03 不允许通过 typedef 定义包含整形模板参数的实例。例如:“typedef CHandle< T, DestoryDeletor<T>, CAtomicINT32, NULL > HT;”语句按照 C++03 标准来说是非法的(假设“NULL”被定义为 0)。using 变种主要就是用来解决这个问题的。但实际上我已经在各种版本的 GCC、VC、Intel C 等编译器上这么用了很多年啊很多年。 因此可以认为这个功能就等同于 typedef。

总结

意思不大,继续用 typedef 就好。如果觉得 using 更别致,那么就请一致地使用(尽量避免混用)。

 

强化的联合(union)类型

说明

在 C++11 中 union 中已可包含部分非 POD 类型,也可以给 union 定义构造函数和方法了。

优点

  • 放宽了 union 对部分非 POD 成员的限制无疑会使其更有用。
  • 允许为其定义构造和方法进一步方便了使用。

缺点

  • 无论如何,由于 union 的先天性质,还是很难让其支持析构方法吧?
  • 其先天性质决定了,一个复杂 union 结构的可读性和可维护性恐怕都不怎么样。

总结

好用的特性,但要注意适可而止,不要把设计搞的太复杂。

 

模板的可变参数

说明

为模板方法提供了类似普通函数的可变数量参数(...)支持。

优点

  • 可极大地方便一些特殊模板的构建,例如:用于实现智能指针模板类中的“operator->*()”(成员指针解引用)操作,以及一些类似于“printf”的模板方法。

缺点

  • 无。

总结

早该有,也是补漏型的特性。

 

新的字符类型和字符串字面值定义

说明

C++11 中新增了 char16_t 和 char32_t 两中类型,用以应对 wchar_t 位宽不确定的问题。又新增了 UTF-8(u8)、UTF-16(u)和 UTF-32(U)字符串字面值定义方法。同时还新增了一种无需对引号(")和反斜杠(\)等特殊字符进行换码的裸字符串字面值定义方法(R),该方法还可以与前面提到的 UTF 字面值前缀结合使用(u8R, uR, UR),非常全面和实用。

优点

  • 明确了 UTF-8、UTF-16 和 UTF-32 字符类型及其字面值定义方法。
  • 对于经常要写些 HTML/XML 或者正则表达式之类字面值的童鞋来说,裸字符串定义方式可以免除换码的烦恼。听上去没啥,但是等 coding 的时候……那是谁用谁知道呀。

缺点

  • 和 wchar_t 及相关旧代码的兼容是个问题。

总结

好用的东东。

 

自定义字面值后缀

说明

就像“1000ul”表示“值为 1000 的无符号长整数”字面值一样,C++11 允许用户通过新的“operator ""”操作自定义字面值后缀解释器,例如:
CMyClass operator "" _mysuf(const char* literal_string);
CMyClass iVar = 1234_mysuf;

优点

  • 用户可以方便地直接通过字面值来生成自定义类型的对象。

缺点

  • 不当使用将导致代码的可读性和可维护性急剧下降。

总结

这涉及到 coding 时方便与阅读/维护时方便之间的权衡,应慎用。

 

线程本地存储(TLS)

说明

新标准中,“thread_local”存储类可用来指定一个 TLS。

优点

  • C++11 中规定任何可以使用“static”存储类的地方,都可以使用 thread_local。也就是说,可以用它来修饰带有构造、析构方法的对象,使得 TLS 用起来非常简便。

缺点

  • 从 thread_local 语义到实际操作系统 TLS Slot API 之间的封装和映射可能带来额外的运行时开销。

总结

好用的东东。比起直接使用操作系统的 TLS Slot 虽然可能增加少许隐式的开销,但通常对 TLS 存取也没有太高的效率要求(当然至少要比使用互斥量等锁算法要高效才行)。

 

显式默认和禁用方法

说明

C++11 中,用户可以通过“= default”后缀修饰符,为每个类显式指定默认构造函数。也可以通过“= delete”后缀修饰构造函数和赋值等操作(通常用来实现禁止复制的语义)。

优点

  • 能够通过明确的方式显式限定这些特殊方法有助于增强代码的可读性和可维护性。

缺点

  • 无。

总结

是传统的,通过将拷贝构造和赋值操作声明为 private 来禁止复制或类似做法的最佳替代品。

 

long long 类型

说明

C++11 中新增了确保至少 64 bit 的 long long 类型。

优点

  • 无。

缺点

  • 无。

总结

还有什么可说的呢?同样是一个早就该有,而且各大编译器早已支持的特性。

 

static_assert

说明

类似 assert,但专为模板而生:不像 assert 在运行时才检查,static_assert 在模板实例化时执行。

优点

  • 可在实例化点对模板参数进行校验。

缺点

  • 无。

总结

描述和检查模板契约的好方法。

 

允许 sizeof 操作直接作用于类的成员

说明

允许 sizeof 操作直接作用于的成员,例如:“sizeof CMyClass::m_nValue”。

优点

  • 个别模板定义会变得方便一点。

缺点

  • 无。

总结

C++03 虽然不允许类似:“sizeof CMyClass::m_nValue”的写法,但也允许 sizeof 作用与对象的成员,例如:“CMyClass iTest; return sizeof(iTest.a)”之类。而且除了事先不知道类型的模板以外,还真没什么地方需要这么用 sizeof 的。

 

返回目录


标准库

标准库的变化大体可分为以下三种类型:
  1. 为适应新的核心语言而做出的调整和改进,比如:为容器实现基于右值引用的移动语义构造方法、为模板使用 decltype 和可变参数、新增 UTF-32(Windows)或 UTF-16(linux/un*x)字符串支持等等。
  2. 为了支持或更好地配合核心语言而新增的特性,例如 tuple、initializer_list 等等。
  3. 新增加的功能。
  4. 给像散列表(std::unordered_set、std::unordered_map……)之类早已烂大街的东东一个名分,以慰其在天之灵。

以下,我们仅针对上述第三种情况进行选择性讨论:

线程支持

说明

新的标准库提供了对线程、互斥量、条件变量、原子量和内存屏障(参考:多处理器环境和线程同步的高级话题) 的基本支持。但显然不足以应付那些需要长期、稳定、高效运行的 Deamon / Service 开发任务。

优点

  • 勉勉强强可以凑合用的线程开发环境。
  • 提供了比较完整的原子量操作接口,甚至支持 Acquire / Release 语义的内存屏障。

缺点

  • 仅提供最基本的线程操作,对线程的超时等待、挂起/恢复、优先级/调度算法切换/抢占式调度设置、处理器粘滞性(CPU Affinity)/最佳处理器设置、保留栈/提交栈尺寸设置、时间片调度控制(如:放弃当前时间片、强制切换到其它线程等)、以及线程对象自动销毁(fire and forget 语义,不只是 detach 模式)等重要特性缺乏足够支持,难以支撑真实的使用环境。仅提供一个 native_handle() 方法将一切负担扔给用户无疑是一种很不负责任的表现,与 Stroustrup 在《The C++ Programming Language》中再三强调的“标准库要么就不提供一种特性,要么就完整地提供关于该特性的所有操作”之理念完全背道而驰。
     
  • 没有提供获取在线和配置处理器数量以及处理器分组情况(如:Package Node、NUMA Node、SMT/HT Unit)等重要信息的工具。这些都是涉及到能否真正用好线程/线程池的关键信息。
     
  • 提供了好用的递归互斥量,这很好。但是:
    • 互斥量到底使用高效的 FUTEX / Critical Secion 还是低效的 kernel 实现完全交给了库的作者决定。
    • 缺乏超时等待(超时 Lock)操作。
    • 缺乏自旋锁(Spin lock)支持。实测表明,在多处理环境下,使用自旋锁与 FUTEX 配合的方案,可以将锁效率提升几十至数百倍。
       
  • 缺乏信号量支持,虽然可以通过互斥量和条件变量模拟信号量操作,但这无疑会对性能产生较大的影响。
     
  • POSIX 标准规定,无论成功与否,条件变量的 wait 操作都不保证其 mutex 参数的状态,其状态需要在 wait 返回后由用户自行判定。不知道标准库是在这方面是怎么定义的?
     
  • 所有同步工具都仅提供了无名版本——不支持有名互斥量和有名条件变量。
     
  • 不支持线程池,需要用户自己搞定。而用户要通过既有的工具实现支持超时等待、挂起/恢复、优先级/调度算法切换、处理器粘滞性(CPU Affinity)等等特性的线程池模块基本上是痴心妄想。
     
  • 不支持消息队列、不支持生产者/消费者锁、不支持读者/写者锁。顺便提一下:对于这些较为复杂的锁算法,Spin Lock/FUTEX/FUSEM 之类技巧对效率的提升就更为明显,因为每次上锁、解锁都要涉及多个同步对象的操作。

总结

原子量操作的接口完整度颇高,算是整个线程库中唯一让人欣慰地方。但是效率具体如何,还要看各厂商标准库的表现了。根据经验,5 年内各主流编译器厂商貌似很难把一个新加入标准库的组件优化到接近最佳状态。

至于原子量以外的其余部分……也算可供业余玩家赏玩一番,廖解寂寞吧。

 

正则表达式

说明

新的标准库支持正则搜索和替换。

优点

  • 正则是一个无比常用的功能,不支持都没天理了。

缺点

  • 正则也是一个对效率很敏感的功能,其效率跟底层正则引擎关系甚大。优秀的正则引擎使用 DFA/NFA 和各种优化算法提升匹配效率。
  • 如果标准库只支持 POSIX BRE / ERE 标准的正则,那无疑是不够的。如果支持扩展的语法,标准中有没有明确定义?是 Vim 风格?Perl 风格?还是 TCL ARE 风格?
  • 对 Unicode 支持如何,例如:字符类 “\d”或 “[:digit:]”能否匹配中文全角“123”以及中文字符“一二三壹贰叁”等等?这部分在 C++11 标准中有明确定义还是要看各厂商的心情?

总结

如果要用于一个严肃项目的话,那么在正式使用前还需考察一段时间,最起码要搞清楚几个主流厂商的支持程度和实现方式。

 

智能指针

说明

TR1 终于提供了支持引用计数的智能指针。

优点

  • 终于来了,可喜可贺。

缺点

  • 虽可自定义销毁策略,但也使得每个智能指针对象中多了个一个 deletor 成员,对于智能指针这种本身尺寸就很小的句柄类来说,多一个 deletor 成员是个大负担。其实销毁策略完全可以通过模板参数传递,这样不占用内存空间和也没有任何额外的运行时开销。
  • 强制使用原子量,无法自定义引用计数的类型——对于某些仅需要在单线程环境内使用的智能指针,使用普通的整形做引用计数即可。这有时能节省大量开销。
  • 不支持引用计数操作(比如经典的 AddRef / Release)、静态语义和所有权转换等高效操作。 对于新手,屏蔽到这些高级操作有助于产出更稳定的代码,但对于老手来说,这些操作可以带来更高的效率和更多的用法。
  • 不支持 NULL 值自定义——就好像不是所有对象都通过 delete 操作销毁一样,也不是所有“指针”的无效值都是 nullptr。例如:在使用智能指针来管理文件或 socket 句柄时,其 deletor 应该是 close / closesocket / CloseHandle 之类的调用,而其“NULL”值应该是无效句柄(通常是 -1,而不是 NULL 对应的 0 值。相反,0 值一般是合法句柄)。

总结

应付大多数情况足以,对效率和功能有特殊要求的情形下仍有不足。

 

随机数生成器

说明

新库中提供了优质伪随机数生成器。

优点

  • 肯定比传统的 rand 要优质。

缺点

  • 到底有多优质?在 Windows 平台上是使用 Crypto API 取种子的吗?在 un*x 平台上是使用 /dev/random 或其变体取种子吗?随机数生成的具体算法是什么?算法有多健壮?算法效率如何?一切都有待标准库作者定夺?

总结

要真正用于密码编码学领域的话,还需观望。要用在高速大批量生成随机数的场合(比如:填充硬盘)也需观望。