Professional Documents
Culture Documents
04
04
C++11:感觉像是门新语言
C++11 引入了大量令人眼花缭乱的语言特性,包括:
- 内存模型——一个高效的为现代硬件设计的底层抽象,作为描述并发的基础([§4.1.1](#411-内存模型))
- `auto` 和 `decltype`——避免类型名称的不必要重复([§4.2.1](#421-auto-和-decltype))
- 范围 `for`——对范围的简单顺序遍历([§4.2.2](#422-范围-for))
- 移动语义和右值引用——减少数据拷贝([§4.2.3](#423-移动语义))
- 统一初始化—— 对所有类型都(几乎)完全一致的初始化语法和语义([§4.2.5](#425-统一初始化))
- `nullptr`——给空指针一个名字([§4.2.6](#426-nullptr))
- `constexpr` 函数——在编译期进行求值的函数([§4.2.7](#427-constexpr-函数))
- 用户定义字面量——为用户自定义类型提供字面量支持([§4.2.8](#428-用户定义字面量))
- 原始字符串字面量——不需要转义字符的字面量,主要用在正则表达式中([§4.2.9](#429-原始字符串字面
量))
- 属性——将任意信息同一个名字关联([§4.2.10](#4210-属性))
- lambda 表达式——匿名函数对象([§4.3.1](#431-lambda-表达式))
- 变参模板——可以处理任意个任意类型的参数的模板([§4.3.2](#432-变参模板))
- 模板别名——能够重命名模板并为新名称绑定一些模板参数([§4.3.3](#433-别名))
- `noexcept`——确保函数不会抛出异常的方法([§4.5.3](#453-noexcept-规约))
- `override` 和 `final`——用于管理大型类层次结构的明确语法
- `static_assert`——编译期断言
- `long long`——更长的整数类型
- 默认成员初始化器——给数据成员一个默认值,这个默认值可以被构造函数中的初始化所取代
- `enum class`——枚举值带有作用域的强类型枚举
以下是主要的标准库组件列表([§4.6](#46-c11 标准库组件)):
这些表面上互不相干的扩展怎么能组成一个连贯的整体?这怎么可能真正地改变我们写代码的方式,使之变得更
好呢?C++11 确实做到了这一点。在相对较短的时间里(算 5 年吧),大量的 C++ 代码被升级到 C++11(并
进一步升级到 C++14 和 C++17),而且 C++ 在会议和博客上的呈现也完全改变了。
这种在语言的“感觉”和使用风格上的巨大变化,并不是由某位大师级工匠指导的传统的精心设计过程的结果,
而是海量建议经由一大批不断变化的个人层层决策过滤后的结果。
- [§4.1](#41-c11 并发支持):支持并发
- [§4.2](#42-c11 简化使用):简化使用
- [§4.3](#43-c11 改进对泛型编程的支持):改进对泛型编程的支持
- [§4.4](#44-c11 提高静态类型安全):提高静态类型安全
- [§4.5](#45-c11 支持对库的开发):支持对库的开发
- [§4.6](#46-c11 标准库组件):标准库组件
## 4.1 C++11:并发支持
- [§4.1.1](#411-内存模型):内存模型
- [§4.1.2](#412-线程和锁):线程和锁
- [§4.1.3](#413-期值 future):期值
此外,并行算法([§8.5](08.md#85-并行-stl))、网络([§8.8.1](08.md#881-网络库))和协程
([§9.3.2](09.md#932-协程))是单独分组处理的,并且(正如预期)还没法用于 C++11。
最紧迫的问题之一,是在一个有着多核、缓存、推测执行、指令乱序等的世界里精确地规定访问内存的规则。来
自 IBM 的 Paul McKenney 在内存保证方面的课题上非常活跃。来自剑桥大学的 Mark Batty 的研究
[Batty et al. 2013, 2012, 2010, 2011] 帮助我们将这一课题形式化,见 P. McKenney、M.
Batty、C. Nelson、H. Boehm、A. Williams、S. Owens、S. Sarkar、P. Sewell、T. Weber、M.
Wong、L. Crowl 和 B. Kosnik 合作的论文 [McKenney et al. 2010]。它是 C++11 的一个庞大而至
关重要的部分。
不出所料,并发组的内存模型讨论有时变得有点激烈。这关系到硬件制造商和编译器供应商的重大利益。最困难
的决定之一是同时接受英特尔的 x86 原语(某种全存储顺序,Total Store Order(TSO)模型 [TSO
Wikipedia 2020] 加上一些原子操作)和 IBM 的 PowerPC 原语(弱一致性加上内存屏障)用于最底层的同
步。从逻辑上讲,只需要一套原语,但 Paul McKenney 让我相信,对于 IBM,有太多深藏在复杂算法中的代
码使用了屏障,他们不可能采用类似英特尔的模型。有一天,我真的在一个大房间的两个角落之间做了穿梭外交。
最后,我提出必须支持这两种方式,这就是 C++11 采用的方式。当后来人们发现内存屏障和原子操作可以一起
使用,创造出比单单使用其中之一更好的解决方案时,我和其他人都感到非常高兴。
稍后,我们增加了对基于数据依赖关系的一致性支持,通过属性([§4.2.10](#4210-属性))在源代码中表示,
比如 `[[carries_dependency]]`。
```cpp
atomic<int> x;
void increment()
{
x++; // 不是 x = x + 1
}
```
显然,这些都是广泛有用的。例如,使用原子类型使出名棘手的双重检查锁定优化变得极为简单:
```cpp
mutex mutex_x;
atomic<bool> init_x; // 初始为 false
int x;
if (!init_x) {
lock_guard<mutex> lck(mutex_x);
if (!init_x) x = 42;
init_x = true ;
} // 在此隐式释放 mutex_x(RAII)
// ... 使用 x ...
```
```cpp
template<typename T>
class stack {
std::atomic<node<T>*> head;
public:
void push(const T& data)
{
node<T>* new_node = new node<T>(data);
new_node->next = head.load(std::memory_order_relaxed);
while(!head.compare_exchange_weak(new_node->next, new_node,
std::memory_order_release, std::memory_order_relaxed)) ;
}
// ...
};
```
```cpp
class F { // 传统函数对象
public:
F(const vector<double>& vv, double* p) : v{vv}, res{p} { }
void operator()(); // 将结果放入 *res
private:
const vector<double>& v; // 输入源
double* res; // 输出目标
};
t1.join();
t2.join();
t3.join();
cout << res1 << ' ' << res2 << ' ' << res3 << '\n';
}
```
类型安全库支持的设计非常依赖变参模板([§4.3.2](#432-变参模板))。例如,`std::thread` 的构造函
数就是变参模板。它可以区分不同的可执行的第一个参数,并检查它们后面是否跟有正确数量正确类型的参数。
在发布标准的同时,让新特性在标准库中被接受和使用是很困难的。有人提出这样做过于激进,可能会导致长期
问题。引入新的语言特性并同时使用它们无疑是有风险的,但它通过以下方式大大增加了标准的质量:
- 给用户一个更好的标准库
- 给用户一个很好的使用语言特性的例子
- 省去了用户实现底层功能的麻烦
- 迫使语言特性的设计者应对现实世界的困难应用
```cpp
mutex m; // 控制用的互斥锁
int sh; // 共享的数据
void access ()
{
unique_lock<mutex> lck {m}; // 得到互斥锁
sh += 7; // 操作共享数据
} // 隐式释放互斥锁
```
这些锁对象还提供了一种防止最常见形式的死锁的方法:
```cpp
void f()
{
// ...
unique_lock<mutex> lck1 {m1,defer_lock}; // 还未得到 m1
unique_lock<mutex> lck2 {m2,defer_lock};
unique_lock<mutex> lck3 {m3,defer_lock};
// ...
lock(lck1,lck2,lck3); // 获取所有三个互斥锁
// ... 操作共享数据 ...
} // 隐式释放所有互斥锁
```
最后,C++11 提供了:
```cpp
double comp4(vector<double>& v)
// 如果 v 足够大则会产生多个任务
{
if (v.size()<10000) // 值得用并发机制吗?
return accum(v.begin(),v.end(),0.0);
auto v0 = &v[0];
auto sz = v.size();
auto f0 = async(accum,v0,v0+sz/4,0.0); // 第一部分
auto f1 = async(accum,v0+sz/4,v0+sz/2,0.0); // 第二部分
auto f2 = async(accum,v0+sz/2,v0+sz*3/4,0.0); // 第三部分
auto f3 = async(accum,v0+sz*3/4,v0+sz,0.0); // 第四部分
```cpp
X f(Y); // 普通函数
为简单起见,我没有使用参数的完美转发([§4.2.3](#423-移动语义))。
```cpp
void user(Y arg)
{
auto pro = promise<X>{};
auto fut = pro.get_future();
thread t {ff,arg,ref(pro)}; // 在不同线程上运行 ff
// ... 做一会别的事情 ...
X x = fut.get();
cout << x.x << '\n';
t.join();
}
int main()
{
user(Y{99});
}
```
我曾希望这会产生一个由线程池支持的工作窃取(work-stealing)的实现,但我还是失望了。
另见([§8.4](08.md#84-并发))。
## 4.2 C++11:简化使用
当然,新事物的拥护者不可避免地认为他们的设计简单、易用、足够安全、高效、易于传授,及对大多数程序员
有用。反对者则倾向于怀疑他们的部分甚至全部说法。但是,确保对 C++ 提议的每个特性都经历这样的讨论是
很重要的:可以通过面对面会议,可以通过论文 [WG21 1989–2020],也可以通过电子邮件。在这些讨论中,
我经常指出,我大部分时间也是个新手。也就是说,当我学习新的特性、技巧或应用领域时,我是一个新手,我
会用到从语言和标准库中可以获得的所有帮助。一个结果是,C++11 提供了一些特别的功能,旨在简化初学者和
非语言专家对 C++ 的使用。
每一项新特性都会让一些人做某些事时更加简单。“简化使用”的主题聚焦于这样一些语言特性,它们的主要设
计动机是让已知的惯用法使用起来更加简单。下面列举其中的一些:
- [§4.2.1](#421-auto-和-decltype):`auto`——避免类型名称的不必要重复
- [§4.2.2](#422-范围-for):范围 `for`——简化范围的顺序遍历
- [§4.2.3](#423-移动语义):移动语义和右值引用——减少数据拷贝
- [§4.2.4](#424-资源管理指针):资源管理指针——管理所指向对象生命周期的“智能”指针
(`unique_ptr` 和 `shared_ptr`)
- [§4.2.5](#425-统一初始化):统一初始化——对所有类型都(几乎)完全一致的初始化语法和语义
- [§4.2.6](#426-nullptr):`nullptr`——给空指针一个名字
- [§4.2.7](#427-constexpr-函数):`constexpr` 函数——编译期被估值的函数
- [§4.2.8](#428-用户定义字面量):用户定义字面量——为用户自定义类型提供字面量支持
- [§4.2.9](#429-原始字符串字面量):原始字符串字面量——转义字符(`\`)不被解释为转义符的字面量,
主要用在正则表达式中
- [§4.2.10](#4210-属性):属性——将任意信息同一个名字关联
- [§4.2.11](#4211-垃圾收集):与可选的垃圾收集器之间的接口
- [§4.3.1](#431-lambda-表达式):lambda 表达式——匿名函数对象
- [§4.2.1](#421-auto-和-decltype):`auto`
- [§4.2.2](#422-范围-for):范围 `for`
- [§4.3.1](#431-lambda-表达式):lambda 表达式
这三个特性属于 C++11 中新增的最简单特性,它们并不能提供任何新的基础功能。它们做的事情,在 C++98
中也能做到,只是不那么优雅。
我认为这意味着不同水平的程序员都非常喜欢让惯常用法变简洁的写法。他们会高兴地放弃一个通用的写法,而
选择一个在适用场合中更简单明确的写法。有一个常见的口号是,“一件事只应有一种说法![^1]”这样的“设
计原则”根本不能反映现实世界中的用户偏好。我则倾向于依赖**洋葱原则** [Stroustrup 1994]。你的设
计应该是这样的:如果要完成的任务是简单的,那就用简单的方法做;当要完成的任务不是那么简单时,就需要
更详细、更复杂的技巧或写法。这就好比你剥下了一层洋葱。剥得越深,流泪就越多。
请注意,这里**简单**并不意味着**底层**。`void*`、宏、C 风格字符串和类型转换等底层功能表面上学起
来简单,但使用它们来产出高质量、易维护的软件就难了。
C++11 中最古老的新特性,是能够在初始化的时候就给对象指定一个确定的类型。例如:
```cpp
auto i = 7; // i 是个整数
auto d = 7.2; // d 是个双精度浮点数
auto p = v.begin(); // p 是 v 的迭代器类型
// (begin() 返回一个迭代器)
```
`auto` 是一个方便的静态特性,它允许从初始化表达式中推导出对象的静态类型。如果要用动态类型的变量,
应该使用 `variant` 或者 `any`([§8.3](08.md#83-variantoptional-和-any))。
```cpp
typeof(x+y) z = y+x;
```
在这里,我以为我重复计算了 `x+y`,但其实并没有(潜在的不良影响),但不管怎么样,我为什么要把任何东
西重复写两遍呢?这时候我意识到,我其实在 1982 年就解决过这个问题,我们可以“劫持”关键字 `auto`
来消除这种重复:
```cpp
auto z = y+x; // z 获得 y+x 的类型
```
剩下的问题是,我们要在某些场景中把引用的类型也推导为一个引用。这在基于模板的基础库中并不少见。我们
提出了用 `decltype` 运算符来处理这种保留引用的语义:
```cpp
template<typename T> void f(T& r)
{
auto v = r; // v 是 T
decltype(r) r2 = r; // r2 是 T&
// ...
}
```
为什么是 `decltype`?可惜,我已经不记得是谁建议了这个名字了,但是我还记得原因:
- `typeof` 已经不能用了,因为那样会破坏很多老代码
- 我们找不到其他优雅、简短、且没有被用过的名字了
- `decltype` 足够好记(“declared type”的简写);但也足够古怪,因而没有在现有代码中用过
- `decltype` 还算比较短
```cpp
auto f(auto arg)
{
return arg;
}
当我向委员会提出这个想法时,我收到了超过我的任何其他提案的负面反馈。我形容当时的情景“就像贵妇见到
了老鼠一样”,他们叫嚷着:“咦咿……!”。然而,故事还没结束。C++17 后来对 lambda 表达式
([§4.3.1](#431-lambda-表达式))的参数和返回值都支持了 `auto`,而对普通的函数,C++17 只支持
返回值的 `auto`。作为概念的一部分([§6.4](06.md#64-c20-概念)),C++20 为函数参数添加了
`auto` 支持,至此才完全实现了我在 2003 年提出的建议。
C++11 中添加了一种弱化的 `auto` 用法,把返回类型的说明放到参数后面。例如,在 C++98 中,我们会这
样写:
```cpp
template<typename T>
vector<T>::iterator vector<T>::begin() { /* ... */ }
```
```cpp
template<typename T>
auto vector<T>::begin() -> iterator { /* ... */ }
```
这样,在多年努力后,我们终于有了 `auto`。它立即就变得非常流行,因为它让程序员不用再拼写冗长的类型
名称,也不需要在泛型代码中考虑类型的细节。例如:
```cpp
for (auto p = v.begin(); p != v.end(); ++p) ... // 传统的 STL 循环
```
它允许人们对齐名字:
```cpp
class X {
public:
auto f() -> int;
auto gpr(int) -> void;
// ...
};
void use(int x, char* p)
{
auto x2 = x*2; // x2 是 int
auto ch = p[x]; // ch 是 char
auto p2 = p+2; // p2 是 char*
// ...
}
```
```cpp
auto n = 1; // 很好:n 是 int
auto x = make_unique<Gadget>(arg); // 很好:x 是 std::unique_ptr<Gadget>
auto y = flopscomps(x,3); // 不好:flopscomps() 返回的是啥东西?
```
这仍然无法百分百地确定如何在每种情况下应用该规则,但有规则总比没有规则要好得多,并且代码会比使用绝
对规则“不许使用 `auto`!”和“永远使用 `auto`!”更加可读。真实世界的编程往往需要更多的技巧,不
会像展示语言特性的例子这样简单。
```cpp
Channel auto y = flopscomps(x,3); // y 可以当做 Channel 使用
```
对 C++ 标准委员会通过哪怕是最小的功能所需的时间,以及常伴其间的痛苦讨论,经常让我感到绝望。但是另
一方面,把事情做好之后,成千上万的程序员会从中受益。当某件事做得很好时,最常见的评论是:“这很明显
啊!怎么你们要花那么久?”
范围 `for` 是用来顺序遍历一个序列中所有元素的语句。例如:
```cpp
void use(vector<int>& v, list<string>& lst)
{
for (int x : v) cout << x << '\n';
int sum = 0;
for (auto i : {1,2,3,5,8}) sum+=i; // 初始化列表是一个序列
for (string& s : lst) s += ".cpp"; // 使用引用允许遍历时修改
}
```
```cpp
void use(vector<int>& v, list<string>& lst)
{
for (int i=0; i<imax; ++i)
for (int j=0; i<imax; ++j) ... // 错误的嵌套循环
在 C 和 C++ 中,要从函数获得大量的数据,传统做法是在自由存储区(堆、动态内存)上分配空间,然后传递
指向该空间的指针作为函数参数。比如,对于工厂函数和返回容器(例如 `vector` 和 `map`)的函数就需要
如此。这对开发者来说看起来很自然,而且相当高效。不幸的是,它是显式使用指针的主要来源之一,导致了写
法上的不便、显式的内存管理,以及难以查找的错误。
多年来,很多专家使用“取巧”的办法来解决这个问题:把句柄类作为简单数值(常称为**值类型**)来传递,
例如:
```cpp
Matrix operator+(const Matrix&, const Matrix&);
这里 `operator+` 让我们可以使用常规的数学写法,同时也是一个工厂函数返回大对象的示例。
要做到这一点,需要观察到“大对象”通常是在自由存储区上的数据的一个句柄。为了避免复制大量的数据,我
们只需要确保在实现返回时,构造函数复制的只是句柄,而不是所有元素。C++11 对这个问题的解决方案如下所
示:
```cpp
class Matrix {
double* elements; // 指向所有元素的指针
// ...
public:
Matrix (Matrix&& a) // 移动构造
{
elements = a.elements; // 复制句柄
a.elements = nullptr; // 现在 a 的析构函数不用做任何事情了
}
// ...
};
```
当用于初始化或赋值的源对象马上就会被销毁时,**移动**就比**拷贝**要更好:移动操作只是简单地把对象的
内部表示“窃取”过来。`&&` 表示构造函数是一个**移动构造函数**,`Matrix&&` 被称为**右值引用**。
当用于模板参数时,右值引用的写法 `&&` 被叫做**转发引用**,这是由 John Spicer 在 2002 年的一次
会议上,同 Dave Abrahams 和 Howard Hinnant 一起提出的。
移动语义蕴含着性能上的重大好处:它消除了代价高昂的临时变量。例如:
```cpp
Matrix mx = m1+m2+m3; // 不需要临时变量
string sx = s1+s2+s3; // 不需要临时变量
```
允许类的设计者定义移动操作后,我们就有了完整的对对象生命周期和资源管理的控制,这套控制始于 1979 年
对构造函数和析构函数的引入。移动语义是 C++ 资源管理模型的重要基石 [Stroustrup et al. 2015],正
是这套机制使得对象能够在不同作用域之间简单而高效地进行移动。
早期对参数传递、完美转发和智能指针强调颇多,可能掩盖了这个重要的一般性观点。Howard Hinnant、Dave
Abrahams 和 Peter Dimov 在 2002 年提出了移动语义的一般化版本 [Hinnant et al. 2004,
2002]:
> 右值引用可以用于给现有类方便地添加移动语义。意思是说,拷贝构造函数和赋值运算符可以根据实参是左值
还是右值来进行重载。当实参是右值时,类的作者就知道他拥有对该实参的唯一引用。
一个突出的例子是生成“智能指针”的工厂函数:
```cpp
template <class T, class A1>
std::shared_ptr<T> factory(A1&& a1)
{
return std::shared_ptr<T>(new T(std::forward<A1>(a1)));
}
```
```cpp
template <class T>
class clone_ptr
{
private:
T* ptr;
public:
// ...
clone_ptr(clone_ptr&& p) // 移动构造函数
: ptr(p.ptr) // 拷贝数据的表示
{
p.ptr = 0; // 把源数据的表示置空
}
clone_ptr& operator=(clone_ptr&& p) // 移动赋值
{
std::swap(ptr, p.ptr);
return *this; // 销毁目标的旧值
}
};
```
很快,移动语义技术就被应用到了标准库的所有容器类上,像 `vector`、`string` 和
`map`。`shared_ptr` 和 `unique_ptr` 的确智能,但它们仍然是指针。我更喜欢强调移动构造和移动赋
值,它们使得(以句柄表示的)大型对象在作用域间能够高效移动。
右值引用的提案在委员会中涉险过关。有人认为右值引用和移动语义多半来不及进入 C++11,因为这些概念很新,
而我们那时连合适的术语都没有。部分由于术语上的问题 [Miller 2010],**右值引用**这一术语在核心语言
和标准库中的使用就有了分歧,从而使得标准草案中出现了不一致。在 2010 年 3 月的匹兹堡会议上,我参与
了核心工作组(CWG)的讨论,在午饭休息的时间,在我看来“我们陷入了僵局,或者混乱之中,也许兼而有之”。
我没有去吃午饭,而是对问题进行了分析,并得出结论,这里只涉及到两个基本概念:**有标识符**
(identity),及**可被移动**。从这两个原语出发,我推导出了传统的左值和右值类别 [Barron et al.
1963],以及解决我们的定义问题所需要的三个新类别。在核心工作组回来之后,我提出了我的解决方案。它很
快就得到了接受,这样我们就在 C++11 中保留了移动语义 [Stroustrup 2010a]。
C++11 提供了“智能指针”([§4.2.4](#424-资源管理指针)):
- `shared_ptr`——代表共享所有权
- `unique_ptr`——代表独占所有权(取代 C++98 中的 `auto_ptr`)
添加这些表示所有权的资源管理“智能指针”对编程风格有很大的影响。对很多人来说,这意味着不再有资源泄
漏,悬空指针的问题也显著减少。在自动化资源管理和减少裸指针使用的努力中,它们是最明显的部分了
([§4.2.3](#423-移动语义))。
`shared_ptr` 是传统的计数指针:指向同一对象的所有指针共享一个计数器。当最后一个指向对象的共享指针
被销毁时,被指向的对象也会被销毁。这是一种简单、通用且有效的垃圾收集形式。它能正确地处理非内存资源
([§2.2.1](02.md#221-语言特性))。为了正确处理环形数据结构,还需要有 `weak_ptr`;不过,这往往
不是最好的做法。人们常常简单地使用 `shared_ptr` 来安全地从工厂函数返回数据:
```cpp
shared_ptr<Blob> make_Blob(Args a)
{
auto p = shared_ptr<Blob>(new Blob(a));
// ... 把很多好东西填到 *p ...
return p;
}
```
当把对象移出函数时,引用计数会从 1 变到 2 再变回 1。在多线程程序中,这通常是涉及到同步的缓慢操作。
另外,粗率地使用和/或实现引用计数,会增加分配和回收的开销。
正如预期的那样,`shared_ptr` 很快就流行起来,并在有些地方被严重滥用。因此,后来我们提供了不引入额
外开销的 `unique_ptr`。`unique_ptr` 对它所指的对象拥有独占的所有权,并会在自身被销毁的时候把指
向的对象也简单地 `delete` 掉。
```cpp
unique_ptr<Blob> make_Blob(Args a)
{
auto p = unique_ptr<Blob>(new Blob(a));
// ... 把很多好东西填到 *p ...
return p;
}
```
资源管理指针被广泛地用于持有对象,以便异常(及类似的情况)不会导致资源泄漏([§2.2](02.md#22-第二
个十年))。例如:
```cpp
void old_use(Args a)
{
auto q = new Blob(a);
// ...
if (foo) throw Bad(); // 会泄漏
if (bar) return; // 会泄漏
// ...
delete q; // 容易忘
}
```
```cpp
void newer_use(Args a)
{
auto p = unique_ptr<Blob>(new Blob(a));
// ...
if (foo) throw Bad(); // 不会泄漏
if (bar) return; // 不会泄漏
// ...
}
```
这种写法更简短、更安全,迅速就流行开去。不过,“智能指针”仍然被过度使用:“它们的确智能,但它们仍
然是指针。”除非我们确实需要指针,否则,简单地使用局部变量会更好:
```cpp
void simplest_use(Args a)
{
Blob b(a);
// ...
if (foo) throw Bad(); // 不会泄漏
if (bar) return; // 不会泄漏
// ...
}
```
智能指针用于表示资源所有权的主要用途是面向对象编程,其中指针(或引用)用于访问对象,而对象的确切类
型在编译时并不知道。
出于历史原因,C++ 有多种初始化的写法,而它们的语义有惊人的不同。
从 C 语言中,C++ 继承了三种初始化形式,并添加了第四种形式:
```cpp
int x; // 默认初始化(仅适用于静态变量)
int x = 7; // 值初始化
int a[] = {7,8}; // 聚合初始化
string s; // 由默认构造函数初始化
vector<int> v(10); // 由构造函数初始化
```
用于初始化的概念既取决于要初始化的对象的类型,也取决于初始化的上下文。这是一团乱麻,而且人们也认识
到这一点。比如,为什么可以用列表初始化内建数组,但却不能初始化 `vector`?
```cpp
int a[] = {7,8}; // 可以
vector<int> v = {7,8}; // 应该可以工作(显然,但是没有)
```
```cpp
int a = {5}; // 内建类型
int a[] {7,8}; // 数组
vector<int> v = {7,8}; // 具有构造函数的用户定义的类型
```
花括号(`{}`)对于单个值是可选的,并且花括号初始化器列表之前的 `=` 也是可选的。为了统一起见,在许
多 C++98 不允许使用花括号或者 `=` 初始化的地方都接受花括号样式的初始化:
```cpp
int f(vector<int>);
int i = f({1,2,3}); // 函数参数
struct X {
vector<int> v;
int a[];
X() : v{1,2}, a{3,4} {} // 成员初始化器
X(int);
// ...
}
可惜,对于这一理想,我们仅仅达到不完全的近似,我们有的方案只能算大致统一。有些人发现,使用 `{…}`
很别扭,除非 `…` 是同质对象的列表,而其他人则坚持 C 语言中对聚合和非聚合的区分,并且许多人担心没有
显式类型标记的列表会导致歧义和错误。例如,以下写法被认为是危险的,不过最终还是被接受了:
```cpp
struct S { string s; int i; };
S foo(S s)
{
// ...
return {string{"foo"},13};
}
S x = foo({string{"alpha"},12.3});
```
在一种情况下,对统一写法的追求被一种惯用法击败。考虑:
```cpp
vector<int> v1(10); // 10 个元素
vector<int> v2 {10}; // 10 个元素还是 1 个值为 10 的元素?
vector<int> v3 {1,2,3,4,5}; // 拥有 5 个元素的 vector
```
```cpp
vector<int> v1 {Extent{10}}; // 10 个元素,默认值为 0
vector<int> v2 {10}; // 1 个元素,值为 10
```
但是,C++ 并不是一门新语言,因此我们决定,在构造函数中进行选择时优先选择初始化器列表解释。这使
`vector<int> v2 {10}` 成为具有一个元素的 `vector`,并且使 `{…}` 初始化器的解释保持一致。但是,
当我们想要避免使用初始化器列表构造函数时,这就迫使我们使用 `(…)` 写法。
初始化的问题之一正在于,它无处不在,因此基本上所有程序和语言规则的问题都会在初始化上下文中体现出来。
考虑:
```cpp
int x = 7.2; // 传统的初始化
int y {7.2}; // 花括号初始化
```
```cpp
double d = 7.2;
int x = d; // 可以:截断
int y {d}; // 错误
```
这是一个常见问题的例子。人们想要一条简单的升级路径,但是除非需要做出一些努力和更改,否则一次非常简
单的升级的结果是,旧的问题和错误得以保留。改善一门广泛使用的语言比我们一般想像的要难。
与以往一样,写法是一个有争议的问题,但是最终我们同意有一个标准库类型的 `initializer_list` 用作
初始化器列表构造函数的参数类型。举例来说:
```cpp
template<typename T> class vector {
public:
vector(initializer_list<T>); // 初始化器列表构造函数
// ...
};
令人遗憾的是,统一初始化(`{}` 初始化)的使用并不像我期望的那样广泛。人们似乎更喜欢熟悉的写法和熟
悉的缺陷。我似乎陷入了 N+1 问题:你有 N 个不兼容和不完整的解决方案,因此添加了一个新的更好的解决方
案。不幸的是,原始的 N 个解决方案并没有消失,所以你现在有了 N+1 个解决方案。公平地说,有一些细微的
问题超出了本文的范围,这些问题只是在 C++14、C++17 和 C++20 中被逐步补救。我的印象是,泛型编程和
对更简洁写法的普遍推动正在慢慢增加统一初始化的吸引力。所有标准库容器(如 `vector`)都有初始化器列
表构造函数。
```cpp
int* p = 99-55-44; // 空指针
int* q = 2; // 错误:2 是一个 int,而不是一个指针
```
```cpp
int p0 = nullptr;
int* p1 = 99-55-44; // 可以,为了兼容性
int* p2 = NULL; // 可以,为了兼容性
int f(char*);
int f(int);
- 让编译期计算达到类型安全
- 一般来说,通过将计算移至编译期来提高效率
- 支持嵌入式系统编程(尤其是 ROM)
- 直接支持元编程(而非**模板**元编程([§10.5.2](10.md#1052-元编程)))
- 让编译期编程与“普通编程”非常相似
```cpp
struct LengthInKM {
constexpr explicit LengthInKM(double d) : val(d) { }
constexpr double getValue() { return val; }
private:
double val;
};
struct LengthInMile {
constexpr explicit LengthInMile(double d) : val(d) { }
constexpr double getValue() { return val; }
constexpr operator LengthInKM() { return LengthInKM(1.609344 * val); }
private:
double val;
};
```
有了这些,我们可以制作一个常量表,而不必担心单位错误或转换错误:
```cpp
LengthInKM marks[] = { LengthInMile(2.3), LengthInMile(0.76) };
```
传统的解决方案要么需要更多的运行时间,要么需要程序员在草稿纸上算好值。我对单位制的兴趣是由 1999 年
的火星气候探测者号的失事激发的,事故原因是单位不匹配没有被发现 [Stephenson et al. 1999]。
```cpp
constexpr LengthInKM marks[] = { LengthInMile(2.3), LengthInMile(0.76) };
void f(int x)
{
int y1 = x;
constexpr int y2 = x; // 错误:x 不是一个常量
constexpr int y3 = 77; // 正确
}
```
“用户定义字面量”是一个非常小的功能。但是,它合乎我们的总体目标,即让用户定义类型得到和内建类型同等
的支持。内建类型有字面量,例如,`10` 是整数,`10.9` 是浮点数。我试图说服人们,对于用户定义类型,
显式地使用构造函数是等价的方式;举例来说,`complex<double>(1.2,3.4)` 就是 `complex` 的字面量
等价形式。然而,许多人认为这还不够好:写法并不传统,而且不能保证构造函数在编译期被求值(尽管这还是
早年间的事)。对于 `complex`,人们想要 `1.2+3.4i`。
与其他问题相比,这似乎并不重要,所以几十年来什么都没有发生。2006 年的一天,David
Vandevoorde(EDG)、Mike Wong(IBM)和我在柏林的一家中餐馆吃了一顿丰盛的晚餐。我们在餐桌边聊起
了天,于是一个设计浮现在一张餐巾纸上。这个讨论的起因是 IBM 的一项十进制浮点提案中对后缀的需求,该
提案最终成了一个独立的国际标准 [Klarer 2007]。在大改后,该设计在 2008 年成为**用户定义字面量**
(通常称为 UDL)[McIntosh et al. 2008]。当时让 UDL 变得有趣的重要发展是 `constexpr` 提案的
进展([§4.2.7](#427-constexpr-函数))。有了它,我们可以保证编译期求值。
```cpp
constexpr Imaginary operator""i(long double x) { return Imaginary(x); }
```
这一功能的语言技术细节相当古怪,但我认为对于一个相对很少使用的特性来说,这是合理的。即使在大量使用
UDL 时,字面量运算符的定义也很少。最重要的是后缀的优雅和易用性。对于许多类型,重要的是可以在编译时
完成从内建类型到用户定义类型的转换。
```cpp
regex pattern1 {"\\w{2}\\s*\\d{5}(-\\d{4})?"}; // 普通字符串字面量
### 4.2.10 属性
在程序中,属性提供了一种将本质上任意的信息与程序中的实体相关联的方法。例如:
```cpp
[[noreturn]] void forever()
{
for (;;) {
do_work();
wait(10s);
}
}
```
该提案提到了可能的使用:覆盖虚函数的明确语法,动态库,用户控制的垃圾收集,线程本地存储,控制对齐,
标识“简旧数据”(POD)类,default 和 delete 的函数,强类型枚举,强类型 typedef,无副作用的纯
函数,final 覆盖,密封类,对并发性的细粒度控制,运行期反射支持,及轻量级契约编程主持。在早期的讨论
中还提到了更多。
“属性”当然是一个使某些事情变得更简单的特性,但我不确定它是否鼓励了良好的设计,或者它简化的“事情”
总是能产生最大的好处。我可以想象属性打开了闸门,放进来一大堆不相关的、不太为人们了解的、次要的特性。
任何人都可以为编译器添加一个属性,并游说各处采用它,而不是向 WG21 提出一个特性。许多程序员就是喜欢
这些小特性。它不需要引入关键字和修改语法,这可以降低门槛,但也更容易不可避免地导致对特性交互关注度
不够,造成重叠而不兼容的类似特性出现在不同的编译器中。这种情况在私有扩展中已经发生过了,但我认为私
有扩展是不可避免的、局部的,而且往往是暂时的。
为了限制潜在的损害,我们决定属性应该意味着不改变程序的语义。也就是说,忽略属性,编译器不会有任何危
害。多年来,这条“规则”几乎奏效。大多数标准属性——尽管不是全部——没有语义效果,即使它们有助于优
化和错误检测。
最后,大多数最初那些建议的对属性的使用都通过普通的语法和语言规则来解决。
C++20 增加了
`[[likely]]`、`[[unlikely]]`、`[[deprecated(message)]]`、`[[no_unique_address]]` 和
`[[using: …]]`。
我仍然看到属性扩散是一个潜在的风险,但到目前为止,水闸还没有打开。C++ 标准库大量使用了属性;
`[[nodiscard]]` 属性尤其受欢迎,特别用来防止由于没有使用本身是资源句柄的返回值而造成的潜在资源泄
漏。
属性语法被用于(失败的)C++20 契约设计([§9.6.1](09.md#961-契约))。
> 同时支持垃圾收集实现和基于可达性的泄漏检测器。这是通过把“隐藏指针”的程序定为未定义行为来实现的;
举例来说,将指针与另一个值进行异或运算,然后将它转换回普通指针并对其进行解引用就是一种隐藏行为。
## 4.3 C++11:改进对泛型编程的支持
- 超越以 C 风格或面向对象风格所可能获得的灵活性
- 更清晰的代码
- 更细的静态类型检查粒度
- 效率(主要来自内联、让编译器同时查看多处的源代码,以及更好的类型检查)
C++11 中支持泛型编程的主要新特性有:
- [§4.3.1](#431-lambda-表达式):lambda 表达式
- [§4.3.2](#432-变参模板):变参模板
- [§4.3.3](#433-别名):`template` 别名
- [§4.3.4](#434-tuple):`tuple`
- [§4.2.5](#425-统一初始化):统一初始化
在 C++11 中,概念本应是改进支持泛型编程的核心,但这并没有发生([§6.2.6](06.md#626-哪里出错
了))。我们不得不等到 C++20([§6.4](06.md#64-c20-概念))。
- 把代码写在需要它的那个准确位置上(通常作为函数参数)。
- 从代码内部访问代码的上下文。
在 C++98 的开发过程中,曾有人提议使用局部函数来解决第二点,但被投票否决了,因为这可能成为缺陷的来
源。
C++ 不允许在函数内部定义函数,而是依赖于在类内部定义的函数。这使得函数的上下文可以表示为类成员,因
而函数对象变得非常流行。**函数对象**只是一个带有调用运算符(`operator()()`)的类。这曾是一种非常
高效和有效的技术,我(和其他人)认为有名字的对象比未命名的操作更清晰。然而,只有当我们可以在某样东
西使用的上下文之外给它一个合理的名称,特别是如果它会被使用多次时,这种清晰度上的优势才会表现出来。
2002 年,Jaakko Järvi 和 Gary Powell 编写了 Boost.Lambda 库 [Järvi and Powell 2002] 这
让我们可以写出这样的东西
```cpp
find_if(v.begin(), v.end(), _1<i); // 查找值小于 i 的元素
```
```cpp
struct Less_than {
int& i;
Less_than(int& ii) :i(ii) {} // 绑定到 i
bool operator()(int x) { return x<i; } // 跟参数比较
}
```
lambda 表达式库是早期模板元编程的典范([§10.5.2](10.md#1052-元编程)),非常方便和流行。不幸的
是,它的效率并不特别高。多年来,我追踪了它相对于手工编码的同等实现的性能,发现它的开销是后者的 2.5
倍且这种差距相当一致。我不能推荐一种方便但却很慢的东西。这样做会损害 C++ 作为产生高效代码的语言的
声誉。显然,这种慢在一定程度上是由于优化不当造成的,但出于这个和其他原因,我们有一群人在 Jaakko
Järvi 领导下决定将 lambda 表达式作为一种语言特性 [Willcock et al. 2006] 来提出。举例来说:
```cpp
template<typename Oper>
void g(Oper op)
{
int xx = op(7);
// ...
}
void f()
{
int y = 3;
g(<>(int x) -> int {return x + y;}); // 以 lambda 表达式作为参数调用 g()
}
```
这一提议引起了相当多的兴奋和许多热烈的讨论:
- 语法应该是富有表现力的还是简洁的?
- lambda 表达式可以从哪个作用域引用什么名字?[Crowl 2009]。
- 从 lambda 表达式生成的函数对象应该是可变的吗?默认情况下不是。
- lambda 表达式能是多态的吗?到 C++14 才可以([§5.4](05.md#54-泛型-lambda-表达式))。
- lambda 表达式的类型是什么?独有的类型,除非它基本上是一个局部函数。
- lambda 表达式可以有名字吗?不可以。如果你需要一个名字,就把它赋给一个变量。
- 名称是由值绑定还是由引用绑定?你来选择。
- 变量可以移动到 lambda 表达式中(相对于复制)吗?到 C++14 才可以([§5](05.md#5-c14 完成-
c11))。
- 语法是否会与各种非标准扩展发生冲突?(不严重)。
```cpp
void abssort(float* x, unsigned N)
{
std::sort(x, x+N,
[](float a, float b) { return std::abs(a) < std::abs(b); }
);
}
```
默认情况下,lambda 表达式不能引用在本地环境的名字,所以它们只是普通的函数。然而,我们可以指定
lambda 表达式应该从它的环境中“捕获”一些或所有的变量。回调是 lambda 表达式的一个常见用例,因为
操作通常只需要写一次,并且操作会需要安装该回调的代码上下文中的一些信息。考虑:
```cpp
void test()
{
string s;
// ... 为 s 计算一个合适的值 ...
w.foo_callback([&s](int i){ do_foo(i,s); });
w.bar_callback([=s](double d){ return do_bar(d,s); });
}
```
lambda 表达式的实现基本上是编译器构建一个合适的函数对象并传递它。捕获的局部变量成为由构造函数初始
化的成员,lambda 表达式的代码成为函数对象的调用运算符。例如,`bar_callback` 变成:
```cpp
struct __XYZ {
string s;
__XYZ(const string& ss) : s{ss} {}
int operator()(double d) { return do_bar(d,s); }
};
```
```cpp
// 按降序排序:
sort(v.begin(),v.end(),[](int x, int y) { return x>y; });
```
因此,lambda 表达式显著地增加了泛型编程的吸引力。
> 直接解决两个问题:
> - 不能实例化包含任意长度参数列表的类模板和函数模板。
> - 不能以类型安全的方式传递任意个参数给某个函数
这些都是重要目标,但我起初发现其解决方案过于复杂,写法太过晦涩,按我的品味其编程风格又太递归。不过
在 Douglas Gregor 于 2004 年做的精彩演示之后,我改变了主意并全力支持这项提案,帮助它在委员会顺
利通过。我被说服的部分原因是变参模板和当时的变通方案在编译时间上的对比测量。编译时间过长的问题随模
板元编程的大量使用([§10.5.2](10.md#1052-元编程))变得越来越严重,对此变参模板是一项重大(有时
是 20 倍)改进。可惜,变参模板越变越流行,也成了 C++ 标准库中必需的部分,以至编译时间的问题又出现
了。不过,成功的惩罚(在当时)还是在遥远的将来。
变参模板的基本思路是,递归构造一个**参数包**,然后在另一个递归过程来使用它。递归技巧是必须的,因为
参数包中的每个元素都有它自己的类型(和大小)。
```cpp
template<typename T, typename... Args>
void printf(const char* s, const T& value, const Args&... args)
{
while (*s) {
if (*s == '%' && *++s != '%') { // 忽略 % 后的字符:
// 我们已经知道要打印的类型了!
std::cout << value;
return printf(++s, args...);
}
std::cout << *s++;
}
throw std::runtime_error("extra arguments provided to printf");
}
```
```cpp
const char* msg = "The value of %s is about %g (unless you live in %s).\n";
printf(msg, std::string("pi"), 3.14159, "Indiana");
```
结果会打印
```
The value of pi is about 3.14159 (unless you live in Indiana).
```
### 4.3.3 别名
C 定义类型别名的机制是靠 `typedef`。例如:
```cpp
typedef double (*pf)(int); // pf 是一个函数指针,该函数接受一个 int
// 返回一个 double
```
```cpp
template<typename A, typename B> class X { /* ... */ };
template<typename T> typedef X<T,int> Xi; // 定义别名
Xi<double> Ddi; // 相当于 X<double, int>
```
```cpp
template<typename T, typename A> class MyVector { /* ... */};
template<typename T> using Vec = MyVector<T, MyAlloc<T> >;
```
```cpp
typedef double (*analysis_fp)(const vector<Student_info>&);
类型和模板别名是某些最有效的零开销抽象及模块化技巧的关键。别名让用户能够使用一套标准的名字而同时让
各种实现使用各自(不同)的实现技巧和名字。这样就可以在拥有零开销抽象的同时保持方便的用户接口。考虑
某通讯库(利用了 Concepts TS [Sutton 2017] 和 C++20 的写法简化)中的一个实例:
```cpp
template<InputTransport Transport, MessageDecoder MessageAdapter>
class InputChannel {
public:
using InputMessage = MessageAdapter::InputMessage<Transport::InputBuffer>;
using MessageCallback = function<void(InputMessage&&)>;
using ErrorCallback = function<void(const error_code&)>;
// ...
};
```
概念和别名对于规模化地管理这样的组合极有价值。
`InputChannel` 的用户接口主要由三个别名组成,`InputMessage`、`MessageCallback` 和
`ErrorCallback`,它们由模板的参数初始化而来。
```cpp
template<InputTransport Transport, MesssageDecoder MessageAdapter>
class InputChannel {
public:
template<typename... TransportArgs>
InputChannel(TransportArgs&&... transportArgs)
: _transport {forward<TransportArgs>(transportArgs)... }
{}
// ...
Transport _transport;
}
```
如果没有变参模板,就得定义出一个通用接口来初始化传输层,或者得把传输层暴露给用户。
> 元组是大小固定而成员类型可以不同的容器。作为一种通用的辅助工具,它们增加了语言的表现力。举几个元
组类型一般用法的例子:
> - 作为返回类型,用于需要超过一个返回类型的函数
> - 编组相关的类型或对象(如参数列表中的各条目)成为单个条目
> - 同时赋多个值
对于特定的设计意图,定义一个类,并在里面对成员进行合理命名、清晰表述成员间的语义关系,通常会是最好
的做法。Alisdair Meredith 在委员会内力陈以上观点,劝阻在接口中过度使用未命名的类型。然而,当撰写
泛型代码时,把多个值打包到一个元组中作为一个实体进行处理往往能简化实现。元组对于不值得命名、不值得
设计类的一些中间情况特别有用。
比如,考虑一个只需返回三个值的矩阵分解:
```cpp
auto SVD(const Matrix& A) -> tuple<Matrix, Vector, Matrix>
{
Matrix U, V;
Vector S;
// ...
return make_tuple(U,S,V);
};
void use()
{
Matrix A, U, V;
Vector S;
// ...
tie(U,S,V) = SVD(A); // 使用元组形式
}
```
使用 C++17 的结构化绑定([§8.2](08.md#82-结构化绑定)),上面例子可简化为:
```cpp
auto SVD(const Matrix& A) -> tuple<Matrix, Vector, Matrix>
{
Matrix U, V;
Vector S;
// ...
return {U,S,V};
};
void use()
{
Matrix A;
// ...
auto [U,S,V] = SVD(A); // 使用元组形式和结构化绑定
}
```
```cpp
tuple SVD(const Matrix& A) // 从返回语句中推导出元组模板参数
{
Matrix U, V;
Vector S;
// ...
return {U,S,V};
};
```
为什么 `tuple` 不是语言特性?我不记得当时有人这么问过,尽管一定有人想到过这一点。长期以来(自
1979 年),我们的策略就是,如果能合理地将新特性以库的形式加入 C++,就不要以语言特性加入;如果不能,
就要改进抽象机制使其成为可能。这一策略有显而易见的优势:
- 通常对一个库做试验比对一个语言特性做试验更容易,这样我们就更快地得到更好的反馈。
- 库可以早在所有编译器升级到支持新特性之前就得到严肃使用。
- 抽象机制(类,模板等)上的改进,能在眼前问题之外提供帮助。
参数包就是一个拥有编译器支持接口的元组的例子([§4.3.2](#432-变参模板))。
## 4.4 C++11:提高静态类型安全
依赖静态类型安全有两大好处:
- 明确意图
- 帮助程序员直接表达想法
- 帮助编译器捕获更多错误
- 帮助编译器生成更好的代码。
C++11 中与类型安全直接相关的改进有:
委员会一直收到建议,应当通过禁止不安全特性(例如,废弃像内建数组和类型转换这样的 C 风格特性)来改善
类型安全。然而,移除特性(“取缔”它们)的尝试一再失败,因为用户无视移除的警告并坚持要求实现的提供
者继续支持这些特性。一个更可行的方式似乎是给用户提供使用指南和实施指南的手段,同时保持标准本身继续
和先前的版本兼容([§10.6](10.md#106-编码指南))。
## 4.5 C++11:支持对库的开发
设计 C++ 基础库,往往要在性能和易用性方面同 C++ 及其他语言的内置功能进行竞争。这时,查找规则、重载
决策、访问控制、模板实例化规则等特性之中的微妙之处会组合起来,产生强大的表达能力,但同时也暴露出可
怕的复杂性。
```cpp
template<typename T, typename U>
struct pair {
T first;
U second;
// ...
enable_if<is_copy_assignable<T>::value
&& is_copy_assignable<U>::value,pair&>::type
operator=(const pair&);
//...
};
```
```cpp
template<bool B, typename T = void>
struct enable_if {}; // false 的情况:里面没有 type
template<typename T>
struct enable_if<true, T> { typedef T type; }; // type 是 T
```
SFINAE 的精确规则非常微妙而难以驾驭,但是在用户的不断压力下,它们在 C++11 的发展过程中变得越来越
简单和通用。SFINAE 的一个附带收获是,它从内部显著改善了编译器,因为编译器必须能够从失败的模板实例
化中进行无副作用的回退。这就大大阻止了编译器对非本地状态的使用。
- **语言**:概念([§6](06.md#6-概念)),编译期函数([§4.2.7](#427-constexpr-函数)),
lambda 表达式([§4.3.1](#431-lambda-表达式)),模板别名([§4.3.3](#433-别名)),以及更精确
的模板实例化规范([§4.5.1](#451-实现技巧))。
- **标准库**:`tuple`([§4.3.4](#434-tuple)),类型特征([§4.5.1](#451-实现技巧)),以及
`enable_if`([§4.5.1](#451-实现技巧))。
遗憾的是,概念在 C++11([§6.2](06.md#62-c0x-概念))中失败了,这给(通常复杂得可怕而且容易出错
的)权宜之计留下了生存空间,典型情况会涉及类型特征和 `enable_if`([§4.5.1](#451-实现技巧))。
起初的异常设计没有办法表明某个异常可能会从某函数中抛出。我仍然认为那才是正确的设计。为了让异常为 C+
+98 接纳,我们不得不加入异常规约,来列举一个函数会抛出那些异常 [Stroustrup 1993]。使用异常规约
可选,并会在运行期进行检查。正如我担心的那样,这带来了维护的问题,在展开路径上对异常反复检查增加的
运行期开销,还有源代码膨胀。在 C++11 中,异常规约被废弃 [Gregor 2010],而到了 C++17,我们终于
(一致同意)移除了异常规约这个特性。
一直有人希望能够在编译时检查函数会抛出什么异常。从类型理论的角度,在小规模程序中,在有高速编译器和
对代码完全控制的情况下,那当然行得通。委员会一再拒绝这种想法,原因是它不能扩展到由数十(或更多)组
织维护的百万行代码规模的程序上 [Stroustrup 1994]。参见([§7.4](07.md#74-类型系统的支持))。
没有异常规约,库实现者们就要面对一个性能问题:在许多重要场合,一个库实现者需要知道一个拷贝操作是否
会抛异常。如果会,就必须拿到一份拷贝以避免留下一个无效对象(这样会违犯异常保证 [Stroustrup
1993])。如果不会,我们可以直接写入到目标中。在这种场合,性能的差别可以非常显著,而最简单的异常规
约 `throw()`,什么也不抛出,在此可以帮助判断。于是,在异常规约被弃之不用并最终从标准中移除的时候,
我们基于 David Abrahams 和 Doug Gregor 的提案 [Abrahams et al. 2010; Gregor 2010;
Gregor and Abrahams 2009] 引入了 `noexcept` 概念。
一个 `noexcept` 函数仍会被动态检查。例如:
```cpp
void do_something(int n) noexcept
{
vector<int> v(n);
// ...
}
```
如果 `do_something()` 抛异常,程序会被终止。这样操作恰好非常接近零开销,因为它简单地短路了通常的
异常传播机制。参见([§7.3](07.md#73-noexcept-规约))。
还有一个条件版本的 `noexcept`,用它可以写出这样的模板,其实现依赖于某参数是否会抛异常。这是最初促
成 `noexcept` 的用例。例如,下面代码中,当且仅当 `pair` 的两个元素都有不抛异常的移动构造函数时,
`pair` 的移动构造函数才会声明不抛异常:
```cpp
template<typename First, typename Second>
class pair {
// ...
template <typename First2, typename Second2>
pair(pair<First2, Second2>&& rhs)
noexcept(is_nothrow_constructible<First, First2&&>::value
&& is_nothrow_constructible<Second, Second2&&>::value)
: first(move(rhs.first)),
second(move(rhs.second))
{}
// ...
};
```
在这相对底层和非常通用的层级写出最优代码可不简单。在基础层面上,懂得到底该按位拷贝,该移动,还是该
按成员拷贝,会带来非常大的区别。
## 4.6 C++11:标准库组件
C++ 跟其他现代语言比一直有个小巧的标准库。此外,大多标准库组件都很基础,而不是试图处理应用层面的任
务。不过,C++11 增加了几个关键的库组件来支持特定任务:
- `thread`——基于线程和锁的并发
- `regex`——正则表达式
- `chrono`——时间
- `random`——随机数产生器和分布
设计这些组件,是要服务于一些特定任务。在这些任务中,它们为程序员提供了重大帮助。遗憾的是,这些库来
自不同背景,体现在接口风格上,就出现了差异;除了要灵活和高性能之外它们没有一致的整体设计哲学。C+
+11 在合入一个组件方面没有明晰的标准(C++98 有一些 [Stroustrup 1994])。更准确地说,我们只是从
现有的、已被社区证明成功的组件中接收组件进来。很多组件来自 Boost([§2.3](02.md#23-c-的-
2006))。
`random` 库提供了分布函数和随机数产生器,其复杂性被誉为“每个随机数库都想长成的样子”。但它对初学
者或者一般用户(常需要随机数)并不易用。它在 2002 年由 Jens Maurer [Maurer 2002] 提出,并在
2006 年经由费米国家实验室的一群人修订 [Brown et al. 2006],随即被接受。
```cpp
using namespace std::chrono; // 在子命名空间 std::chrono
auto t0 = system_clock::now();
do_work();
auto t1 = system_clock::now();
cout << duration_cast<milliseconds>(t1-t0).count() << "msec\n";
```
使用如此简单的代码,你可以让大一学生都能感受到不同算法和数据结构的代价差异。`chrono` 为 `thread`
库提供了时间支持([§4.1.2](#412-线程和锁))。
到了 C++20,`chrono` 得到进一步增强,加入了处理日期和时区的功能([§9.3.6](09.md#936-日期和时
区))。C++20 也允许把上面的例子简化为:
```cpp
cout << t1-t0 << '\n';
```