一份宏大的作品,能够带动一个领域的蓬勃发展。C++ 社群在相对沉寂数十年之后迎来新一波浪潮,原因是,作为一门编程工具的最核心,C++ 的语言和标准库都出现巨大的拓展和强化。这一番大变革始自 2011,并分别于 2014、2017 持续进化。业界习惯性地将这些新版本统称为 Modern C++,用以区别“传统” C++。

作为知名书系的最新作品,《Effective Modern C++》的佳质和佳评一如其早期同门作品《Effective C++》和《More Effective C++》。本书延续作者 Scott Meyers 的一贯风格和质量,其最大特质就是,不但告诉我们 How,更用巨大而精良的篇幅告诉我们 Why。作者穷追猛打讲究再三的劲儿,常让我筋疲力尽,痛并快乐地爬行于某个条款之际拍案而叹:“天哪,还有下一页!”

而我,是一个在 C++ 领域已经生活了 25 年的老兵。

是的,我是一个在 C++ 领域已经生活了 25 年的老兵,这意味着我具备相当的 C++ 能力。尽管如此,面对号称全新语言的 Modern C++,我时或也有力不能逮、掩卷长叹的焦躁,特别是面对 Rvalue Reference(右值引用)、Perfect Forwarding(完美转发)、Metaprogramming(元编程)、Type Deduction(类型推导)、Type Traits(型别特征)等艰涩主题的时候。然而正是在特别艰涩的主题上你可以领受本书的巨大价值:如果你想完善根基,本书是你唯一的选择。

———— 侯捷推荐序

第 1 章 型别推导

C++98 仅有一套型别推导规则,用于函数模板。C++11 对这套规则进行了一些改动,并且增加了两套规则,一套用于 auto,另一套用于 decltype。后来,C++14 又扩展了能够运用 autodecltype 的语境。型别推导应用范围的不断普及,使得人们不必再去写下那些不言自明或是完全冗余的型别。它还让 C++ 软件获得更高的适应性,因为在源代码的一个地方对一个型别实施的改动,可以自动通过型别推导传播到其他地方。然而,它也有可能导致写出来的代码较难看懂,因为编译器推导出的型别,可能不像我们所认为的那样显而易见。

想要使用现代 C++ 高效编程,就离不开对于型别推导操作的坚实理解。型别推导涉及的语境实在不胜枚举:在函数模板的调用中,在 auto 现身的大多数场景中,在 decltype 表达式中,特别是在 C++14 中那个神秘莫测的 decltype(auto) 结构中。

本章讨论的是每个 C++ 开发工程师都需要了解的有关型别推导的知识。本章解释了模板型别推导如何运作,auto 的型别推导如何构建在此运作规则之上,以及 decltype 独特的型别推导规则。本章还教你如何迫使编译器来展示其型别推导的结果,从而让你确信该结果如你所愿。

条款 1 理解模板型别推导

1
2
3
4
template <typename T>
void f(ParamType param);

f(expr); // 从 expr 来推导 T 和 ParamType 的型别

在编译期,编译器会通过 expr 推导两个型别:一个是 T 的型别,另一个是 ParamType 的型别,这两个型别往往不一样。因为,ParamType 常会包含了一些饰词,如 const 或引用符号等限定词。

我们很自然地会认为,T 的型别推导结果和传递给函数的实参型别是同一的。但是,这一点并不总是成立。T 的型别推导结果,不仅仅依赖 expr 的型别,还依赖 ParamType 的形式。具体要分三种情况讨论:

情况 1:ParamType 是个指针或引用,但不是个万能指针

1
2
3
4
5
6
7
8
9
10
template <typename T>
void f(T& param); // param 是个引用

int x = 27;
const int cx = x;
const int& rx = x;

f(x); // T 的型别是 int,param 的型别是 int&
f(cx); // T 的型别是 const int,param 的型别是 const int&
f(rx); // T 的型别是 const int,param 的型别是 const int&

cxrx 的示例中,T 被推导为 const int,说明对象的常量性会成为 T 的型别推导结果的组成部分。

rx 具有引用型别,但 T 并未被推导成一个引用。原因在于,rx 的引用性会在型别推导过程中被忽略。

对于 param 型别改成 const T&,由千我们现在会假定 param 具有 const 引用型别,T 的型别推导结果中包含 const 也就没有必要了。

如果 param 是个指针(或指涉到 const 对象的指针)而非引用, 运作方式本质上并无不同。

情况 2:ParamType 是个万能引用

1
2
3
4
5
6
7
8
9
10
11
template <typename T>
void f(T&& param); // param 现在是个万能引用

int x = 27;
const int cx = x;
const int& rx = x;

f(x); // x 是个左值,T 的型别是 int&,param 的型别是 int&
f(cx); // cx 是个左值,T 的型别是 const int&,param 的型别是 const int&
f(rx); // rx 是个左值,T 的型别是 const int&,param 的型别是 const int&
f(27); // 27 是个右值,T 的型别是 int ,param 的型别是 int&&
  • 如果 expr 是个左值,TParamType 都会被推到为左值引用。这个结果具有双重的奇特之处:首先,这是在模板型别推导中,T 被推导为引用型别的唯一情形。其次,尽管在声明时使用的是右值引用语法,它的型别推导结果却是左值引用。
  • 如果 expr 是个右值,则应用“常规”(即情况 1 中的)规则。

条款 24 详尽解释了为何上述例子会产生这样的结果。关键之处在于,万能引用形参的型别推导规则不同于左值引用和右值引用形参。具体地,当遇到万能引用时,型别推导规则会区分实参是左值还是右值。而非万能引用是从来不会作这样的区分的。

情况 3:ParamType 既非指针也非引用

数组实参

函数实参

要点速记

  • 在模板型别推导过程中,具有引用型别的实参会被当成非引用型别来处理。换言之,其引用性会被忽略。
  • 对万能引用形参进行推导时,左值实参会进行特殊处理。
  • 对按值传递的形参进行推导时,若实参型别中带有 constvolatile 饰词,则它们还是会被当作不带 constvolatile 饰词的型别来处理。
  • 在模板型别推导过程中,数组或函数型别的实参会退化成对应的指针,除非它们被用来初始化引用。

第 2 章 auto

第 3 章 转向现代 C++

第 4 章 智能指针

第 5 章 右值引用、移动语义和完美转发

第 6 章 Lambda 表达式

第 7 章 并发 API

第 8 章 微调