Repository: CnTransGroup/EffectiveModernCppChinese
Branch: master
Commit: fd8d55ccd965
Files: 49
Total size: 315.9 KB
Directory structure:
gitextract_tiyi38cu/
├── .github/
│ └── workflows/
│ └── deploy.yml
├── .gitignore
├── README.md
├── book.toml
├── public/
│ └── EffectModernC++.xmind
└── src/
├── 1.DeducingTypes/
│ ├── item1.md
│ ├── item2.md
│ ├── item3.md
│ └── item4.md
├── 2.Auto/
│ ├── item5.md
│ └── item6.md
├── 3.MovingToModernCpp/
│ ├── item10.md
│ ├── item11.md
│ ├── item12.md
│ ├── item13.md
│ ├── item14.md
│ ├── item15.md
│ ├── item16.md
│ ├── item17.md
│ ├── item7.md
│ ├── item8.md
│ └── item9.md
├── 4.SmartPointers/
│ ├── item18.md
│ ├── item19.md
│ ├── item20.md
│ ├── item21.md
│ └── item22.md
├── 5.RRefMovSemPerfForw/
│ ├── item23.md
│ ├── item24.md
│ ├── item25.md
│ ├── item26.md
│ ├── item27.md
│ ├── item28.md
│ ├── item29.md
│ └── item30.md
├── 6.LambdaExpressions/
│ ├── item31.md
│ ├── item32.md
│ ├── item33.md
│ └── item34.md
├── 7.TheConcurrencyAPI/
│ ├── Item35.md
│ ├── item36.md
│ ├── item37.md
│ ├── item38.md
│ ├── item39.md
│ └── item40.md
├── 8.Tweaks/
│ ├── item41.md
│ └── item42.md
├── Introduction.md
└── SUMMARY.md
================================================
FILE CONTENTS
================================================
================================================
FILE: .github/workflows/deploy.yml
================================================
name: github pages
on:
push:
branches:
- master
paths:
- src/**
- book.toml
pull_request:
jobs:
deploy:
runs-on: ubuntu-20.04
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
steps:
- uses: actions/checkout@v2
- name: Setup mdBook
uses: peaceiris/actions-mdbook@v1
with:
mdbook-version: '0.4.18'
# mdbook-version: 'latest'
- run: mdbook build
- name: Deploy
uses: peaceiris/actions-gh-pages@v3
if: ${{ github.ref == 'refs/heads/master' }}
with:
github_token: ${{ secrets.GITHUB_TOKEN }}
publish_dir: ./book
================================================
FILE: .gitignore
================================================
# .gitignore_global
####################################
######## OS generated files ########
####################################
.DS_Store
.DS_Store?
*.swp
._*
.Spotlight-V100
.Trashes
Icon?
ehthumbs.db
Thumbs.db
####################################
############# Packages #############
####################################
*.7z
*.dmg
*.gz
*.iso
*.jar
*.rar
*.tar
*.zip
# Prerequisites
*.d
# Compiled Object files
*.slo
*.lo
*.o
*.obj
# Precompiled Headers
*.gch
*.pch
# Compiled Dynamic libraries
*.so
*.dylib
*.dll
# Fortran module files
*.mod
*.smod
# Compiled Static libraries
*.lai
*.la
*.a
*.lib
# Executables
*.exe
*.out
*.app
.vscode
.netrwhist
.idea
target
book
================================================
FILE: README.md
================================================
# 《Effective Modern C++ 》翻译
[](#backers)
[](#sponsors)
> + 标注“已修订”的章节表示已经没有大致的错误
> + 本书要求读者具有C++基础
> + [PDF格式英文版下载](public/EffectiveModernCpp.pdf),仅供翻译参考
> + **[在线浏览(推荐)](https://cntransgroup.github.io/EffectiveModernCppChinese)**
## 目录
0. [__简介__](src/Introduction.md)
1. __类型推导__
1. [Item 1:理解模板类型推导](src/1.DeducingTypes/item1.md) 已修订
2. [Item 2:理解auto类型推导](src/1.DeducingTypes/item2.md)
3. [Item 3:理解decltype](src/1.DeducingTypes/item3.md)
4. [Item 4:学会查看类型推导结果](src/1.DeducingTypes/item4.md)
2. __auto__
1. [Item 5:优先考虑auto而非显式类型声明](src/2.Auto/item5.md)
2. [Item 6:auto推导若非己愿,使用显式类型初始化惯用法](src/2.Auto/item6.md)
3. __移步现代C++__
1. [Item 7:区别使用()和{}创建对象](src/3.MovingToModernCpp/item7.md) 已修订
2. [Item 8:优先考虑nullptr而非0和NULL](src/3.MovingToModernCpp/item8.md)
3. [Item 9:优先考虑别名声明而非typedefs](src/3.MovingToModernCpp/item9.md)
4. [Item 10:优先考虑限域枚举而非未限域枚举](src/3.MovingToModernCpp/item10.md) 已修订
5. [Item 11:优先考虑使用deleted函数而非使用未定义的私有声明](src/3.MovingToModernCpp/item11.md)
6. [Item 12:使用override声明重写函数](src/3.MovingToModernCpp/item12.md)
7. [Item 13:优先考虑const_iterator而非iterator](src/3.MovingToModernCpp/item13.md)
8. [Item 14:如果函数不抛出异常请使用noexcept](src/3.MovingToModernCpp/item14.md)
9. [Item 15:尽可能的使用constexpr](src/3.MovingToModernCpp/item15.md)
10. [Item 16:让const成员函数线程安全](src/3.MovingToModernCpp/item16.md)
11. [Item 17:理解特殊成员函数的生成](src/3.MovingToModernCpp/item17.md)
4. __智能指针__
1. [Item 18:对于独占资源使用std::unique_ptr](src/4.SmartPointers/item18.md)
2. [Item 19:对于共享资源使用std::shared_ptr](src/4.SmartPointers/item19.md) 已修订
3. [Item 20:当std::shared_ptr可能悬空时使用std::weak_ptr](src/4.SmartPointers/item20.md)
4. [Item 21:优先考虑使用std::make_unique和std::make_shared,而非直接使用new](src/4.SmartPointers/item21.md)
5. [Item 22:当使用Pimpl惯用法,请在实现文件中定义特殊成员函数](src/4.SmartPointers/item22.md)
5. __右值引用,移动语义,完美转发__
1. [Item 23:理解std::move和std::forward](src/5.RRefMovSemPerfForw/item23.md)
2. [Item 24:区别通用引用和右值引用](src/5.RRefMovSemPerfForw/item24.md)
3. [Item 25:对于右值引用使用std::move,对于通用引用使用std::forward](src/5.RRefMovSemPerfForw/item25.md)
4. [Item 26:避免重载通用引用](src/5.RRefMovSemPerfForw/item26.md)
5. [Item 27:熟悉重载通用引用的替代品](src/5.RRefMovSemPerfForw/item27.md)
6. [Item 28:理解引用折叠](src/5.RRefMovSemPerfForw/item28.md)
7. [Item 29:认识移动操作的缺点](src/5.RRefMovSemPerfForw/item29.md)
8. [Item 30:熟悉完美转发失败的情况](src/5.RRefMovSemPerfForw/item30.md)
6. __Lambda表达式__
1. [Item 31:避免使用默认捕获模式](src/6.LambdaExpressions/item31.md)
2. [Item 32:使用初始化捕获来移动对象到闭包中](src/6.LambdaExpressions/item32.md)
3. [Item 33:对于std::forward的auto&&形参使用decltype](src/6.LambdaExpressions/item33.md)
4. [Item 34:优先考虑lambda表达式而非std::bind](src/6.LambdaExpressions/item34.md)
7. __并发API__
1. [Item 35:优先考虑基于任务的编程而非基于线程的编程](src/7.TheConcurrencyAPI/Item35.md)
2. [Item 36:如果有异步的必要请指定std::launch::async](src/7.TheConcurrencyAPI/item36.md)
3. [Item 37:从各个方面使得std::threads unjoinable](src/7.TheConcurrencyAPI/item37.md)
4. [Item 38:关注不同线程句柄析构行为](src/7.TheConcurrencyAPI/item38.md)
5. [Item 39:考虑对于单次事件通信使用void](src/7.TheConcurrencyAPI/item39.md)
6. [Item 40:对于并发使用std::atomic,volatile用于特殊内存区](src/7.TheConcurrencyAPI/item40.md)
8. __微调__
1. [Item 41:对于那些可移动总是被拷贝的形参使用传值方式](src/8.Tweaks/item41.md)
2. [Item 42:考虑就地创建而非插入](src/8.Tweaks/item42.md)
## 其他资源
+ **[在线阅读(推荐,与翻译内容同步更新)](https://cntransgroup.github.io/EffectiveModernCppChinese)**
+ 本书[PDF格式的中文版](./public/translated/translate-zh-combine.pdf)见于此,该版本通常同步或者滞后于当前Markdown文档
+ [Effective C++ Xmind Doc](./public/EffectModernC++.xmind)
## 贡献者
感谢所有参与翻译/勘误/建议的贡献者们~
## 免责声明
译者纯粹出于学习目的与个人兴趣翻译本书,不追求任何经济利益。译者保留对此版本译文的署名权,其他权利以原作者和出版社的主张为准。本译文只供学习研究参考之用,不得公开传播发行或用于商业用途。有能力阅读英文书籍者请购买正版支持。
================================================
FILE: book.toml
================================================
[book]
authors = ["wendajiang"]
language = "zh"
src = "src"
title = "Effective Modern C++"
[output.html]
mathjax-support = true
no-section-label = true
git-repository-url = "https://github.com/CnTransGroup/EffectiveModernCppChinese"
================================================
FILE: src/1.DeducingTypes/item1.md
================================================
# 第1章 类型推导
**CHAPTER 1 Deducing Types**
C++98有一套类型推导的规则:用于函数模板的规则。C++11修改了其中的一些规则并增加了两套规则,一套用于`auto`,一套用于`decltype`。C++14扩展了`auto`和`decltype`可能使用的范围。类型推导的广泛应用,让你从拼写那些或明显或冗杂的类型名的暴行中脱离出来。它让C++程序更具适应性,因为在源代码某处修改类型会通过类型推导自动传播到其它地方。但是类型推导也会让代码更复杂,因为由编译器进行的类型推导并不总是如我们期望的那样进行。
如果对于类型推导操作没有一个扎实的理解,要想写出有现代感的C++程序是不可能的。类型推导随处可见:在函数模板调用中,在大多数`auto`出现的地方,在`decltype`表达式出现的地方,以及C++14中令人费解的应用`decltype(auto)`的地方。
这一章是每个C++程序员都应该掌握的知识。它解释了模板类型推导是如何工作的,`auto`是如何依赖类型推导的,以及`decltype`是如何按照它自己那套独特的规则工作的。它甚至解释了你该如何强制编译器使类型推导的结果可视,这能让你确认编译器的类型推导是否按照你期望的那样进行。
## 条款一:理解模板类型推导
**Item 1: Understand template type deduction**
对于一个复杂系统的用户来说,很多时候他们最关心的是它做了什么而不是它怎么做的。在这一点上,C++中的模板类型推导表现得非常出色。数百万的程序员只需要向模板函数传递实参,就能通过编译器的类型推导获得令人满意的结果,尽管他们中的大多数在被逼无奈的情况下,对于传递给函数的那些实参是如何引导编译器进行类型推导的,也只能给出非常模糊的描述。
如果那些人中包括你,我有一个好消息和一个坏消息。好消息是现在C++最重要最吸引人的特性`auto`是建立在模板类型推导的基础上的。如果你满意C++98的模板类型推导,那么你也会满意C++11的`auto`类型推导。坏消息是当模板类型推导规则应用于`auto`环境时,有时不如应用于template时那么直观。由于这个原因,真正理解`auto`基于的模板类型推导的方方面面非常重要。这项条款便包含了你需要知道的东西。
如果你不介意浏览少许伪代码,我们可以考虑像这样一个函数模板:
````cpp
template
void f(ParamType param);
````
它的调用看起来像这样
````cpp
f(expr); //使用表达式调用f
````
在编译期间,编译器使用`expr`进行两个类型推导:一个是针对`T`的,另一个是针对`ParamType`的。这两个类型通常是不同的,因为`ParamType`包含一些修饰,比如`const`和引用修饰符。举个例子,如果模板这样声明:
````cpp
template
void f(const T& param); //ParamType是const T&
````
然后这样进行调用
````cpp
int x = 0;
f(x); //用一个int类型的变量调用f
````
`T`被推导为`int`,`ParamType`却被推导为`const int&`
我们可能很自然的期望`T`和传递进函数的实参是相同的类型,也就是,`T`为`expr`的类型。在上面的例子中,事实就是那样:`x`是`int`,`T`被推导为`int`。但有时情况并非总是如此,`T`的类型推导不仅取决于`expr`的类型,也取决于`ParamType`的类型。这里有三种情况:
+ `ParamType`是一个指针或引用,但不是通用引用(关于通用引用请参见[Item24](../5.RRefMovSemPerfForw/item24.md)。在这里你只需要知道它存在,而且不同于左值引用和右值引用)
+ `ParamType`是一个通用引用
+ `ParamType`既不是指针也不是引用
我们下面将分成三个情景来讨论这三种情况,每个情景的都基于我们之前给出的模板:
````cpp
template
void f(ParamType param);
f(expr); //从expr中推导T和ParamType
````
### 情景一:`ParamType`是一个指针或引用,但不是通用引用
最简单的情况是`ParamType`是一个指针或者引用,但非通用引用。在这种情况下,类型推导会这样进行:
1. 如果`expr`的类型是一个引用,忽略引用部分
2. 然后`expr`的类型与`ParamType`进行模式匹配来决定`T`
举个例子,如果这是我们的模板,
````cpp
template
void f(T& param); //param是一个引用
````
我们声明这些变量,
````cpp
int x=27; //x是int
const int cx=x; //cx是const int
const int& rx=x; //rx是指向作为const int的x的引用
````
在不同的调用中,对`param`和`T`推导的类型会是这样:
````cpp
f(x); //T是int,param的类型是int&
f(cx); //T是const int,param的类型是const int&
f(rx); //T是const int,param的类型是const int&
````
在第二个和第三个调用中,注意因为`cx`和`rx`被指定为`const`值,所以`T`被推导为`const int`,从而产生了`const int&`的形参类型。这对于调用者来说很重要。当他们传递一个`const`对象给一个引用类型的形参时,他们期望对象保持不可改变性,也就是说,形参是reference-to-`const`的。这也是为什么将一个`const`对象传递给以`T&`类型为形参的模板安全的:对象的常量性`const`ness会被保留为`T`的一部分。
在第三个例子中,注意即使`rx`的类型是一个引用,`T`也会被推导为一个非引用 ,这是因为`rx`的引用性(reference-ness)在类型推导中会被忽略。
这些例子只展示了左值引用,但是类型推导会如左值引用一样对待右值引用。当然,右值只能传递给右值引用,但是在类型推导中这种限制将不复存在。
如果我们将`f`的形参类型`T&`改为`const T&`,情况有所变化,但不会变得那么出人意料。`cx`和`rx`的`const`ness依然被遵守,但是因为现在我们假设`param`是reference-to-`const`,`const`不再被推导为`T`的一部分:
```cpp
template
void f(const T& param); //param现在是reference-to-const
int x = 27; //如之前一样
const int cx = x; //如之前一样
const int& rx = x; //如之前一样
f(x); //T是int,param的类型是const int&
f(cx); //T是int,param的类型是const int&
f(rx); //T是int,param的类型是const int&
```
同之前一样,`rx`的reference-ness在类型推导中被忽略了。
如果`param`是一个指针(或者指向`const`的指针)而不是引用,情况本质上也一样:
```cpp
template
void f(T* param); //param现在是指针
int x = 27; //同之前一样
const int *px = &x; //px是指向作为const int的x的指针
f(&x); //T是int,param的类型是int*
f(px); //T是const int,param的类型是const int*
```
到现在为止,你会发现你自己打哈欠犯困,因为C++的类型推导规则对引用和指针形参如此自然,书面形式来看这些非常枯燥。所有事情都那么理所当然!那正是在类型推导系统中你所想要的。
### 情景二:`ParamType`是一个通用引用
模板使用通用引用形参的话,那事情就不那么明显了。这样的形参被声明为像右值引用一样(也就是,在函数模板中假设有一个类型形参`T`,那么通用引用声明形式就是`T&&`),它们的行为在传入左值实参时大不相同。完整的叙述请参见[Item24](../5.RRefMovSemPerfForw/item24.md),在这有些最必要的你还是需要知道:
+ 如果`expr`是左值,`T`和`ParamType`都会被推导为左值引用。这非常不寻常,第一,这是模板类型推导中唯一一种`T`被推导为引用的情况。第二,虽然`ParamType`被声明为右值引用类型,但是最后推导的结果是左值引用。
+ 如果`expr`是右值,就使用正常的(也就是**情景一**)推导规则
举个例子:
````cpp
template
void f(T&& param); //param现在是一个通用引用类型
int x=27; //如之前一样
const int cx=x; //如之前一样
const int & rx=cx; //如之前一样
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&&
````
[Item24](../5.RRefMovSemPerfForw/item24.md)详细解释了为什么这些例子是像这样发生的。这里关键在于通用引用的类型推导规则是不同于普通的左值或者右值引用的。尤其是,当通用引用被使用时,类型推导会区分左值实参和右值实参,但是对非通用引用时不会区分。
### 情景三:`ParamType`既不是指针也不是引用
当`ParamType`既不是指针也不是引用时,我们通过传值(pass-by-value)的方式处理:
````cpp
template
void f(T param); //以传值的方式处理param
````
这意味着无论传递什么`param`都会成为它的一份拷贝——一个完整的新对象。事实上`param`成为一个新对象这一行为会影响`T`如何从`expr`中推导出结果。
1. 和之前一样,如果`expr`的类型是一个引用,忽略这个引用部分
2. 如果忽略`expr`的引用性(reference-ness)之后,`expr`是一个`const`,那就再忽略`const`。如果它是`volatile`,也忽略`volatile`(`volatile`对象不常见,它通常用于驱动程序的开发中。关于`volatile`的细节请参见[Item40](../7.TheConcurrencyAPI/item40.md))
因此
````cpp
int x=27; //如之前一样
const int cx=x; //如之前一样
const int & rx=cx; //如之前一样
f(x); //T和param的类型都是int
f(cx); //T和param的类型都是int
f(rx); //T和param的类型都是int
````
注意即使`cx`和`rx`表示`const`值,`param`也不是`const`。这是有意义的。`param`是一个完全独立于`cx`和`rx`的对象——是`cx`或`rx`的一个拷贝。具有常量性的`cx`和`rx`不可修改并不代表`param`也是一样。这就是为什么`expr`的常量性`const`ness(或易变性`volatile`ness)在推导`param`类型时会被忽略:因为`expr`不可修改并不意味着它的拷贝也不能被修改。
认识到只有在传值给形参时才会忽略`const`(和`volatile`)这一点很重要,正如我们看到的,对于reference-to-`const`和pointer-to-`const`形参来说,`expr`的常量性`const`ness在推导时会被保留。但是考虑这样的情况,`expr`是一个`const`指针,指向`const`对象,`expr`通过传值传递给`param`:
````cpp
template
void f(T param); //仍然以传值的方式处理param
const char* const ptr = //ptr是一个常量指针,指向常量对象
"Fun with pointers";
f(ptr); //传递const char * const类型的实参
````
在这里,解引用符号(\*)的右边的`const`表示`ptr`本身是一个`const`:`ptr`不能被修改为指向其它地址,也不能被设置为null(解引用符号左边的`const`表示`ptr`指向一个字符串,这个字符串是`const`,因此字符串不能被修改)。当`ptr`作为实参传给`f`,组成这个指针的每一比特都被拷贝进`param`。像这种情况,`ptr`**自身的值会被传给形参**,根据类型推导的第三条规则,`ptr`自身的常量性`const`ness将会被省略,所以`param`是`const char*`,也就是一个可变指针指向`const`字符串。在类型推导中,这个指针指向的数据的常量性`const`ness将会被保留,但是当拷贝`ptr`来创造一个新指针`param`时,`ptr`自身的常量性`const`ness将会被忽略。
### 数组实参
上面的内容几乎覆盖了模板类型推导的大部分内容,但这里还有一些小细节值得注意,比如数组类型不同于指针类型,虽然它们两个有时候是可互换的。关于这个错觉最常见的例子是,在很多上下文中数组会退化为指向它的第一个元素的指针。这样的退化允许像这样的代码可以被编译:
````cpp
const char name[] = "J. P. Briggs"; //name的类型是const char[13]
const char * ptrToName = name; //数组退化为指针
````
在这里`const char*`指针`ptrToName`会由`name`初始化,而`name`的类型为`const char[13]`,这两种类型(`const char*`和`const char[13]`)是不一样的,但是由于数组退化为指针的规则,编译器允许这样的代码。
但要是一个数组传值给一个模板会怎样?会发生什么?
````cpp
template
void f(T param); //传值形参的模板
f(name); //T和param会推导成什么类型?
````
我们从一个简单的例子开始,这里有一个函数的形参是数组,是的,这样的语法是合法的,
````cpp
void myFunc(int param[]);
````
但是数组声明会被视作指针声明,这意味着`myFunc`的声明和下面声明是等价的:
````cpp
void myFunc(int* param); //与上面相同的函数
````
数组与指针形参这样的等价是C语言的产物,C++又是建立在C语言的基础上,它让人产生了一种数组和指针是等价的的错觉。
因为数组形参会视作指针形参,所以传值给模板的一个数组类型会被推导为一个指针类型。这意味着在模板函数`f`的调用中,它的类型形参`T`会被推导为`const char*`:
````cpp
f(name); //name是一个数组,但是T被推导为const char*
````
但是现在难题来了,虽然函数不能声明形参为真正的数组,但是**可以**接受指向数组的**引用**!所以我们修改`f`为传引用:
````cpp
template
void f(T& param); //传引用形参的模板
````
我们这样进行调用,
````cpp
f(name); //传数组给f
````
`T`被推导为了真正的数组!这个类型包括了数组的大小,在这个例子中`T`被推导为`const char[13]`,`f`的形参(该数组的引用)的类型则为`const char (&)[13]`。是的,这种语法看起来又臭又长,但是知道它将会让你在关心这些问题的人的提问中获得大神的称号。
有趣的是,可声明指向数组的引用的能力,使得我们可以创建一个模板函数来推导出数组的大小:
````cpp
//在编译期间返回一个数组大小的常量值(//数组形参没有名字,
//因为我们只关心数组的大小)
template //关于
constexpr std::size_t arraySize(T (&)[N]) noexcept //constexpr
{ //和noexcept
return N; //的信息
} //请看下面
````
在[Item15](../3.MovingToModernCpp/item15.md)提到将一个函数声明为`constexpr`使得结果在编译期间可用。这使得我们可以用一个花括号声明一个数组,然后第二个数组可以使用第一个数组的大小作为它的大小,就像这样:
````cpp
int keyVals[] = { 1, 3, 7, 9, 11, 22, 35 }; //keyVals有七个元素
int mappedVals[arraySize(keyVals)]; //mappedVals也有七个
````
当然作为一个现代C++程序员,你自然应该想到使用`std::array`而不是内置的数组:
````cpp
std::array mappedVals; //mappedVals的大小为7
````
至于`arraySize`被声明为`noexcept`,会使得编译器生成更好的代码,具体的细节请参见[Item14](../3.MovingToModernCpp/item14.md)。
### 函数实参
在C++中不只是数组会退化为指针,函数类型也会退化为一个函数指针,我们对于数组类型推导的全部讨论都可以应用到函数类型推导和退化为函数指针上来。结果是:
````cpp
void someFunc(int, double); //someFunc是一个函数,
//类型是void(int, double)
template
void f1(T param); //传值给f1
template
void f2(T & param); //传引用给f2
f1(someFunc); //param被推导为指向函数的指针,
//类型是void(*)(int, double)
f2(someFunc); //param被推导为指向函数的引用,
//类型是void(&)(int, double)
````
这个实际上没有什么不同,但是如果你知道数组退化为指针,你也会知道函数退化为指针。
这里你需要知道:`auto`依赖于模板类型推导。正如我在开始谈论的,在大多数情况下它们的行为很直接。在通用引用中对于左值的特殊处理使得本来很直接的行为变得有些污点,然而,数组和函数退化为指针把这团水搅得更浑浊。有时你只需要编译器告诉你推导出的类型是什么。这种情况下,翻到[item4](../1.DeducingTypes/item4.md),它会告诉你如何让编译器这么做。
**请记住:**
+ 在模板类型推导时,有引用的实参会被视为无引用,他们的引用会被忽略
+ 对于通用引用的推导,左值实参会被特殊对待
+ 对于传值类型推导,`const`和/或`volatile`实参会被认为是non-`const`的和non-`volatile`的
+ 在模板类型推导时,数组名或者函数名实参会退化为指针,除非它们被用于初始化引用
================================================
FILE: src/1.DeducingTypes/item2.md
================================================
## 条款二:理解`auto`类型推导
**Item 2: Understand `auto` type deduction**
如果你已经读过[Item1](../1.DeducingTypes/item1.md)的模板类型推导,那么你几乎已经知道了`auto`类型推导的大部分内容,至于为什么不是全部是因为这里有一个`auto`不同于模板类型推导的例外。但这怎么可能?模板类型推导包括模板,函数,形参,但`auto`不处理这些东西啊。
你是对的,但没关系。`auto`类型推导和模板类型推导有一个直接的映射关系。它们之间可以通过一个非常规范非常系统化的转换流程来转换彼此。
在[Item1](../1.DeducingTypes/item2.md)中,模板类型推导使用下面这个函数模板
````cpp
template
void f(ParmaType param);
````
和这个调用来解释:
```cpp
f(expr); //使用一些表达式调用f
```
在`f`的调用中,编译器使用`expr`推导`T`和`ParamType`的类型。
当一个变量使用`auto`进行声明时,`auto`扮演了模板中`T`的角色,变量的类型说明符扮演了`ParamType`的角色。废话少说,这里便是更直观的代码描述,考虑这个例子:
````cpp
auto x = 27;
````
这里`x`的类型说明符是`auto`自己,另一方面,在这个声明中:
````cpp
const auto cx = x;
````
类型说明符是`const auto`。另一个:
````cpp
const auto& rx = x;
````
类型说明符是`const auto&`。在这里例子中要推导`x`,`cx`和`rx`的类型,编译器的行为看起来就像是认为这里每个声明都有一个模板,然后使用合适的初始化表达式进行调用:
````cpp
template //概念化的模板用来推导x的类型
void func_for_x(T param);
func_for_x(27); //概念化调用:
//param的推导类型是x的类型
template //概念化的模板用来推导cx的类型
void func_for_cx(const T param);
func_for_cx(x); //概念化调用:
//param的推导类型是cx的类型
template //概念化的模板用来推导rx的类型
void func_for_rx(const T & param);
func_for_rx(x); //概念化调用:
//param的推导类型是rx的类型
````
正如我说的,`auto`类型推导除了一个例外(我们很快就会讨论),其他情况都和模板类型推导一样。
[Item1](../1.DeducingTypes/item1.md)基于`ParamType`——在函数模板中`param`的类型说明符——的不同特征,把模板类型推导分成三个部分来讨论。在使用`auto`作为类型说明符的变量声明中,类型说明符代替了`ParamType`,因此Item1描述的三个情景稍作修改就能适用于auto:
+ 情景一:类型说明符是一个指针或引用但不是通用引用
+ 情景二:类型说明符一个通用引用
+ 情景三:类型说明符既不是指针也不是引用
我们早已看过情景一和情景三的例子:
````cpp
auto x = 27; //情景三(x既不是指针也不是引用)
const auto cx = x; //情景三(cx也一样)
const auto & rx=cx; //情景一(rx是非通用引用)
````
情景二像你期待的一样运作:
```cpp
auto&& uref1 = x; //x是int左值,
//所以uref1类型为int&
auto&& uref2 = cx; //cx是const int左值,
//所以uref2类型为const int&
auto&& uref3 = 27; //27是int右值,
//所以uref3类型为int&&
```
[Item1](../1.DeducingTypes/item1.md)讨论并总结了对于non-reference类型说明符,数组和函数名如何退化为指针。那些内容也同样适用于`auto`类型推导:
````cpp
const char name[] = //name的类型是const char[13]
"R. N. Briggs";
auto arr1 = name; //arr1的类型是const char*
auto& arr2 = name; //arr2的类型是const char (&)[13]
void someFunc(int, double); //someFunc是一个函数,
//类型为void(int, double)
auto func1 = someFunc; //func1的类型是void (*)(int, double)
auto& func2 = someFunc; //func2的类型是void (&)(int, double)
````
就像你看到的那样,`auto`类型推导和模板类型推导几乎一样的工作,它们就像一个硬币的两面。
讨论完相同点接下来就是不同点,前面我们已经说到`auto`类型推导和模板类型推导有一个例外使得它们的工作方式不同,接下来我们要讨论的就是那个例外。
我们从一个简单的例子开始,如果你想声明一个带有初始值27的`int`,C++98提供两种语法选择:
````cpp
int x1 = 27;
int x2(27);
````
C++11由于也添加了用于支持统一初始化(**uniform initialization**)的语法:
````cpp
int x3 = { 27 };
int x4{ 27 };
````
总之,这四种不同的语法只会产生一个相同的结果:变量类型为`int`值为27
但是[Item5](../2.Auto/item5.md)解释了使用`auto`说明符代替指定类型说明符的好处,所以我们应该很乐意把上面声明中的`int`替换为`auto`,我们会得到这样的代码:
````cpp
auto x1 = 27;
auto x2(27);
auto x3 = { 27 };
auto x4{ 27 };
````
这些声明都能通过编译,但是他们不像替换之前那样有相同的意义。前面两个语句确实声明了一个类型为`int`值为27的变量,但是后面两个声明了一个存储一个元素27的 `std::initializer_list`类型的变量。
````cpp
auto x1 = 27; //类型是int,值是27
auto x2(27); //同上
auto x3 = { 27 }; //类型是std::initializer_list,
//值是{ 27 }
auto x4{ 27 }; //同上
````
这就造成了`auto`类型推导不同于模板类型推导的特殊情况。当用`auto`声明的变量使用花括号进行初始化,`auto`类型推导推出的类型则为`std::initializer_list`。如果这样的一个类型不能被成功推导(比如花括号里面包含的是不同类型的变量),编译器会拒绝这样的代码:
````cpp
auto x5 = { 1, 2, 3.0 }; //错误!无法推导std::initializer_list中的T
````
就像注释说的那样,在这种情况下类型推导将会失败,但是对我们来说认识到这里确实发生了两种类型推导是很重要的。一种是由于`auto`的使用:`x5`的类型不得不被推导。因为`x5`使用花括号的方式进行初始化,`x5`必须被推导为`std::initializer_list`。但是`std::initializer_list`是一个模板。`std::initializer_list`会被某种类型`T`实例化,所以这意味着`T`也会被推导。 推导落入了这里发生的第二种类型推导——模板类型推导的范围。在这个例子中推导之所以失败,是因为在花括号中的值并不是同一种类型。
对于花括号的处理是`auto`类型推导和模板类型推导唯一不同的地方。当使用`auto`声明的变量使用花括号的语法进行初始化的时候,会推导出`std::initializer_list`的实例化,但是对于模板类型推导这样就行不通:
````cpp
auto x = { 11, 23, 9 }; //x的类型是std::initializer_list
template //带有与x的声明等价的
void f(T param); //形参声明的模板
f({ 11, 23, 9 }); //错误!不能推导出T
````
然而如果在模板中指定`T`是`std::initializer_list`而留下未知`T`,模板类型推导就能正常工作:
````cpp
template
void f(std::initializer_list initList);
f({ 11, 23, 9 }); //T被推导为int,initList的类型为
//std::initializer_list
````
因此`auto`类型推导和模板类型推导的真正区别在于,`auto`类型推导假定花括号表示`std::initializer_list`而模板类型推导不会这样(确切的说是不知道怎么办)。
你可能想知道为什么`auto`类型推导和模板类型推导对于花括号有不同的处理方式。我也想知道。哎,我至今没找到一个令人信服的解释。但是规则就是规则,这意味着你必须记住如果你使用`auto`声明一个变量,并用花括号进行初始化,`auto`类型推导总会得出`std::initializer_list`的结果。如果你使用**uniform initialization(花括号的方式进行初始化)**用得很爽你就得记住这个例外以免犯错,在C++11编程中一个典型的错误就是偶然使用了`std::initializer_list`类型的变量,这个陷阱也导致了很多C++程序员抛弃花括号初始化,只有不得不使用的时候再做考虑。(在[Item7](../3.MovingToModernCpp/item7.md)讨论了必须使用时该怎么做)
对于C++11故事已经说完了。但是对于C++14故事还在继续,C++14允许`auto`用于函数返回值并会被推导(参见[Item3](../1.DeducingTypes/item3.md)),而且C++14的*lambda*函数也允许在形参声明中使用`auto`。但是在这些情况下`auto`实际上使用**模板类型推导**的那一套规则在工作,而不是`auto`类型推导,所以说下面这样的代码不会通过编译:
````cpp
auto createInitList()
{
return { 1, 2, 3 }; //错误!不能推导{ 1, 2, 3 }的类型
}
````
同样在C++14的lambda函数中这样使用auto也不能通过编译:
````cpp
std::vector v;
…
auto resetV =
[&v](const auto& newValue){ v = newValue; }; //C++14
…
resetV({ 1, 2, 3 }); //错误!不能推导{ 1, 2, 3 }的类型
````
**请记住:**
+ `auto`类型推导通常和模板类型推导相同,但是`auto`类型推导假定花括号初始化代表`std::initializer_list`,而模板类型推导不这样做
+ 在C++14中`auto`允许出现在函数返回值或者*lambda*函数形参中,但是它的工作机制是模板类型推导那一套方案,而不是`auto`类型推导
================================================
FILE: src/1.DeducingTypes/item3.md
================================================
## 条款三:理解`decltype`
**Item 3: Understand decltype**
`decltype`是一个奇怪的东西。给它一个名字或者表达式`decltype`就会告诉你这个名字或者表达式的类型。通常,它会精确的告诉你你想要的结果。但有时候它得出的结果也会让你挠头半天,最后只能求助网上问答或参考资料寻求启示。
我们将从一个简单的情况开始,没有任何令人惊讶的情况。相比模板类型推导和`auto`类型推导(参见[Item1](../1.DeducingTypes/item1.md)和[Item2](../1.DeducingTypes/item2.md)),`decltype`只是简单的返回名字或者表达式的类型:
````cpp
const int i = 0; //decltype(i)是const int
bool f(const Widget& w); //decltype(w)是const Widget&
//decltype(f)是bool(const Widget&)
struct Point{
int x,y; //decltype(Point::x)是int
}; //decltype(Point::y)是int
Widget w; //decltype(w)是Widget
if (f(w))… //decltype(f(w))是bool
template //std::vector的简化版本
class vector{
public:
…
T& operator[](std::size_t index);
…
};
vector v; //decltype(v)是vector
…
if (v[0] == 0)… //decltype(v[0])是int&
````
看见了吧?没有任何奇怪的东西。
在C++11中,`decltype`最主要的用途就是用于声明函数模板,而这个函数返回类型依赖于形参类型。举个例子,假定我们写一个函数,一个形参为容器,一个形参为索引值,这个函数支持使用方括号的方式(也就是使用“`[]`”)访问容器中指定索引值的数据,然后在返回索引操作的结果前执行认证用户操作。函数的返回类型应该和索引操作返回的类型相同。
对一个`T`类型的容器使用`operator[]` 通常会返回一个`T&`对象,比如`std::deque`就是这样。但是`std::vector`有一个例外,对于`std::vector`,`operator[]`不会返回`bool&`,它会返回一个全新的对象(译注:MSVC的STL实现中返回的是`std::_Vb_reference>>`对象)。关于这个问题的详细讨论请参见[Item6](../2.Auto/item6.md),这里重要的是我们可以看到对一个容器进行`operator[]`操作返回的类型取决于容器本身。
使用`decltype`使得我们很容易去实现它,这是我们写的第一个版本,使用`decltype`计算返回类型,这个模板需要改良,我们把这个推迟到后面:
````cpp
template //可以工作,
auto authAndAccess(Container& c, Index i) //但是需要改良
->decltype(c[i])
{
authenticateUser();
return c[i];
}
````
函数名称前面的`auto`不会做任何的类型推导工作。相反的,他只是暗示使用了C++11的**尾置返回类型**语法,即在函数形参列表后面使用一个”`->`“符号指出函数的返回类型,尾置返回类型的好处是我们可以在函数返回类型中使用函数形参相关的信息。在`authAndAccess`函数中,我们使用`c`和`i`指定返回类型。如果我们按照传统语法把函数返回类型放在函数名称之前,`c`和`i`就未被声明所以不能使用。
在这种声明中,`authAndAccess`函数返回`operator[]`应用到容器中返回的对象的类型,这也正是我们期望的结果。
C++11允许自动推导单一语句的*lambda*表达式的返回类型, C++14扩展到允许自动推导所有的*lambda*表达式和函数,甚至它们内含多条语句。对于`authAndAccess`来说这意味着在C++14标准下我们可以忽略尾置返回类型,只留下一个`auto`。使用这种声明形式,auto标示这里会发生类型推导。更准确的说,编译器将会从函数实现中推导出函数的返回类型。
````cpp
template //C++14版本,
auto authAndAccess(Container& c, Index i) //不那么正确
{
authenticateUser();
return c[i]; //从c[i]中推导返回类型
}
````
[Item2](../1.DeducingTypes/item2.md)解释了函数返回类型中使用`auto`,编译器实际上是使用的模板类型推导的那套规则。如果那样的话这里就会有一些问题。正如我们之前讨论的,`operator[]`对于大多数`T`类型的容器会返回一个`T&`,但是[Item1](../1.DeducingTypes/item1.md)解释了在模板类型推导期间,表达式的引用性(reference-ness)会被忽略。基于这样的规则,考虑它会对下面用户的代码有哪些影响:
````cpp
std::deque d;
…
authAndAccess(d, 5) = 10; //认证用户,返回d[5],
//然后把10赋值给它
//无法通过编译!
````
在这里`d[5]`本该返回一个`int&`,但是模板类型推导会剥去引用的部分,因此产生了`int`返回类型。函数返回的那个`int`是一个右值,上面的代码尝试把10赋值给右值`int`,C++11禁止这样做,所以代码无法编译。
要想让`authAndAccess`像我们期待的那样工作,我们需要使用`decltype`类型推导来推导它的返回值,即指定`authAndAccess`应该返回一个和`c[i]`表达式类型一样的类型。C++期望在某些情况下当类型被暗示时需要使用`decltype`类型推导的规则,C++14通过使用`decltype(auto)`说明符使得这成为可能。我们第一次看见`decltype(auto)`可能觉得非常的矛盾(到底是`decltype`还是`auto`?),实际上我们可以这样解释它的意义:`auto`说明符表示这个类型将会被推导,`decltype`说明`decltype`的规则将会被用到这个推导过程中。因此我们可以这样写`authAndAccess`:
````cpp
template //C++14版本,
decltype(auto) //可以工作,
authAndAccess(Container& c, Index i) //但是还需要
{ //改良
authenticateUser();
return c[i];
}
````
现在`authAndAccess`将会真正的返回`c[i]`的类型。现在事情解决了,一般情况下`c[i]`返回`T&`,`authAndAccess`也会返回`T&`,特殊情况下`c[i]`返回一个对象,`authAndAccess`也会返回一个对象。
`decltype(auto)`的使用不仅仅局限于函数返回类型,当你想对初始化表达式使用`decltype`推导的规则,你也可以使用:
````cpp
Widget w;
const Widget& cw = w;
auto myWidget1 = cw; //auto类型推导
//myWidget1的类型为Widget
decltype(auto) myWidget2 = cw; //decltype类型推导
//myWidget2的类型是const Widget&
````
但是这里有两个问题困惑着你。一个是我之前提到的`authAndAccess`的改良至今都没有描述。让我们现在加上它。
再看看C++14版本的`authAndAccess`声明:
````cpp
template
decltype(auto) authAndAccess(Container& c, Index i);
````
容器通过传引用的方式传递非常量左值引用(lvalue-reference-to-non-**const**),因为返回一个引用允许用户可以修改容器。但是这意味着在不能给这个函数传递右值容器,右值不能被绑定到左值引用上(除非这个左值引用是一个const(lvalue-references-to-**const**),但是这里明显不是)。
公认的向`authAndAccess`传递一个右值是一个[edge case](https://en.wikipedia.org/wiki/Edge_case)(译注:在极限操作情况下会发生的事情,类似于会发生但是概率较小的事情)。一个右值容器,是一个临时对象,通常会在`authAndAccess`调用结束被销毁,这意味着`authAndAccess`返回的引用将会成为一个悬置的(dangle)引用。但是使用向`authAndAccess`传递一个临时变量也并不是没有意义,有时候用户可能只是想简单的获得临时容器中的一个元素的拷贝,比如这样:
````cpp
std::deque makeStringDeque(); //工厂函数
//从makeStringDeque中获得第五个元素的拷贝并返回
auto s = authAndAccess(makeStringDeque(), 5);
````
要想支持这样使用`authAndAccess`我们就得修改一下当前的声明使得它支持左值和右值。重载是一个不错的选择(一个函数重载声明为左值引用,另一个声明为右值引用),但是我们就不得不维护两个重载函数。另一个方法是使`authAndAccess`的引用可以绑定左值和右值,[Item24](../5.RRefMovSemPerfForw/item24.md)解释了那正是通用引用能做的,所以我们这里可以使用通用引用进行声明:
````cpp
template //现在c是通用引用
decltype(auto) authAndAccess(Container&& c, Index i);
````
在这个模板中,我们不知道我们操纵的容器的类型是什么,那意味着我们同样不知道它使用的索引对象(index objects)的类型,对一个未知类型的对象使用传值通常会造成不必要的拷贝,对程序的性能有极大的影响,还会造成对象切片行为(参见[item41](../8.Tweaks/item41.md)),以及给同事落下笑柄。但是就容器索引来说,我们遵照标准模板库对于索引的处理是有理由的(比如`std::string`,`std::vector`和`std::deque`的`operator[]`),所以我们坚持传值调用。
然而,我们还需要更新一下模板的实现,让它能听从[Item25](../5.RRefMovSemPerfForw/item25.md)的告诫应用`std::forward`实现通用引用:
````cpp
template //最终的C++14版本
decltype(auto)
authAndAccess(Container&& c, Index i)
{
authenticateUser();
return std::forward(c)[i];
}
````
这样就能对我们的期望交上一份满意的答卷,但是这要求编译器支持C++14。如果你没有这样的编译器,你还需要使用C++11版本的模板,它看起来和C++14版本的极为相似,除了你不得不指定函数返回类型之外:
````cpp
template //最终的C++11版本
auto
authAndAccess(Container&& c, Index i)
->decltype(std::forward(c)[i])
{
authenticateUser();
return std::forward(c)[i];
}
````
另一个问题是就像我在条款的开始唠叨的那样,`decltype`通常会产生你期望的结果,但并不总是这样。在**极少数情况下**它产生的结果可能让你很惊讶。老实说如果你不是一个大型库的实现者你不太可能会遇到这些异常情况。
为了**完全**理解`decltype`的行为,你需要熟悉一些特殊情况。它们大多数都太过晦涩以至于几乎没有书进行有过权威的讨论,这本书也不例外,但是其中的一个会让我们更加理解`decltype`的使用。
将`decltype`应用于变量名会产生该变量名的声明类型。虽然变量名都是左值表达式,但这不会影响`decltype`的行为。(译者注:这里是说对于单纯的变量名,`decltype`只会返回变量的声明类型)然而,对于比单纯的变量名更复杂的左值表达式,`decltype`可以确保报告的类型始终是左值引用。也就是说,如果一个不是单纯变量名的左值表达式的类型是`T`,那么`decltype`会把这个表达式的类型报告为`T&`。这几乎没有什么太大影响,因为大多数左值表达式的类型天生具备一个左值引用修饰符。例如,返回左值的函数总是返回左值引用。
这个行为暗含的意义值得我们注意,在:
````cpp
int x = 0;
````
中,`x`是一个变量的名字,所以`decltype(x)`是`int`。但是如果用一个小括号包覆这个名字,比如这样`(x)` ,就会产生一个比名字更复杂的表达式。对于名字来说,`x`是一个左值,C++11定义了表达式`(x)`也是一个左值。因此`decltype((x))`是`int&`。用小括号覆盖一个名字可以改变`decltype`对于名字产生的结果。
在C++11中这稍微有点奇怪,但是由于C++14允许了`decltype(auto)`的使用,这意味着你在函数返回语句中细微的改变就可以影响类型的推导:
````cpp
decltype(auto) f1()
{
int x = 0;
…
return x; //decltype(x)是int,所以f1返回int
}
decltype(auto) f2()
{
int x = 0;
return (x); //decltype((x))是int&,所以f2返回int&
}
````
注意不仅`f2`的返回类型不同于`f1`,而且它还引用了一个局部变量!这样的代码将会把你送上未定义行为的特快列车,一辆你绝对不想上第二次的车。
当使用`decltype(auto)`的时候一定要加倍的小心,在表达式中看起来无足轻重的细节将会影响到`decltype(auto)`的推导结果。为了确认类型推导是否产出了你想要的结果,请参见[Item4](../1.DeducingTypes/item4.md)描述的那些技术。
同时你也不应该忽略`decltype`这块大蛋糕。没错,`decltype`(单独使用或者与`auto`一起用)可能会偶尔产生一些令人惊讶的结果,但那毕竟是少数情况。通常,`decltype`都会产生你想要的结果,尤其是当你对一个变量使用`decltype`时,因为在这种情况下,`decltype`只是做一件本分之事:它产出变量的声明类型。
**请记住:**
+ `decltype`总是不加修改的产生变量或者表达式的类型。
+ 对于`T`类型的不是单纯的变量名的左值表达式,`decltype`总是产出`T`的引用即`T&`。
+ C++14支持`decltype(auto)`,就像`auto`一样,推导出类型,但是它使用`decltype`的规则进行推导。
================================================
FILE: src/1.DeducingTypes/item4.md
================================================
## 条款四:学会查看类型推导结果
**Item 4: Know how to view deduced types**
选择什麼工具查看类型推导,取决于软件开发过程中你想在哪个阶段显示类型推导信息。我们探究三种方案:在你编辑代码的时候获得类型推导的结果,在编译期间获得结果,在运行时获得结果。
### IDE编辑器
在IDE中的代码编辑器通常可以显示程序代码中变量,函数,参数的类型,你只需要简单的把鼠标移到它们的上面,举个例子,有这样的代码中:
````cpp
const int theAnswer = 42;
auto x = theAnswer;
auto y = &theAnswer;
````
IDE编辑器可以直接显示`x`推导的结果为`int`,`y`推导的结果为`const int*`。
为此,你的代码必须或多或少的处于可编译状态,因为IDE之所以能提供这些信息是因为一个C++编译器(或者至少是前端中的一个部分)运行于IDE中。如果这个编译器对你的代码不能做出有意义的分析或者推导,它就不会显示推导的结果。
对于像`int`这样简单的推导,IDE产生的信息通常令人很满意。正如我们将看到的,如果更复杂的类型出现时,IDE提供的信息就几乎没有什么用了。
### 编译器诊断
另一个获得推导结果的方法是使用编译器出错时提供的错误消息。这些错误消息无形的提到了造成我们编译错误的类型是什么。
举个例子,假如我们想看到之前那段代码中`x`和`y`的类型,我们可以首先声明一个类模板但**不定义**。就像这样:
````cpp
template //只对TD进行声明
class TD; //TD == "Type Displayer"
````
如果尝试实例化这个类模板就会引出一个错误消息,因为这里没有用来实例化的类模板定义。为了查看`x`和`y`的类型,只需要使用它们的类型去实例化`TD`:
````cpp
TD xType; //引出包含x和y
TD yType; //的类型的错误消息
````
我使用***variableName*****Type**的结构来命名变量,因为这样它们产生的错误消息可以有助于我们查找。对于上面的代码,我的编译器产生了这样的错误信息,我取一部分贴到下面:
````cpp
error: aggregate 'TD xType' has incomplete type and
cannot be defined
error: aggregate 'TD yType' has incomplete type and
cannot be defined
````
另一个编译器也产生了一样的错误,只是格式稍微改变了一下:
````cpp
error: 'xType' uses undefined class 'TD'
error: 'yType' uses undefined class 'TD'
````
除了格式不同外,几乎所有我测试过的编译器都产生了这样有用的错误消息。
### 运行时输出
使用`printf`的方法(并不是说我推荐你使用`printf`)类型信息要在运行时才会显示出来,但是它提供了一种格式化输出的方法。现在唯一的问题是对于你关心的变量使用一种优雅的文本表示。“这有什么难的,“你这样想,”这正是`typeid`和`std::type_info::name`的价值所在”。为了实现我们想要查看`x`和`y`的类型的需求,你可能会这样写:
````cpp
std::cout << typeid(x).name() << '\n'; //显示x和y的类型
std::cout << typeid(y).name() << '\n';
````
这种方法对一个对象如`x`或`y`调用`typeid`产生一个`std::type_info`的对象,然后`std::type_info`里面的成员函数`name()`来产生一个C风格的字符串(即一个`const char*`)表示变量的名字。
调用`std::type_info::name`不保证返回任何有意义的东西,但是库的实现者尝试尽量使它们返回的结果有用。实现者们对于“有用”有不同的理解。举个例子,GNU和Clang环境下`x`的类型会显示为”`i`“,`y`会显示为”`PKi`“,这样的输出你必须要问问编译器实现者们才能知道他们的意义:”`i`“表示”`int`“,”`PK`“表示”pointer to ~~`konst`~~ `const`“(指向常量的指针)。(这些编译器都提供一个工具`c++filt`,解释这些“混乱的”类型)Microsoft的编译器输出得更直白一些:对于`x`输出”`int`“对于`y`输出”`int const *`“
因为对于`x`和`y`来说这样的结果是正确的,你可能认为问题已经接近了,别急,考虑一个更复杂的例子:
````cpp
template //要调用的模板函数
void f(const T& param);
std::vector createVec(); //工厂函数
const auto vw = createVec(); //使用工厂函数返回值初始化vw
if (!vw.empty()){
f(&vw[0]); //调用f
…
}
````
在这段代码中包含了一个用户定义的类型`Widget`,一个STL容器`std::vector`和一个`auto`变量`vw`,这个更现实的情况是你可能会遇到的并且想获得他们类型推导的结果,比如模板类型形参`T`,比如函数`f`形参`param`。
从这里中我们不难看出`typeid`的问题所在。我们在`f`中添加一些代码来显示类型:
````cpp
template
void f(const T& param)
{
using std::cout;
cout << "T = " << typeid(T).name() << '\n'; //显示T
cout << "param = " << typeid(param).name() << '\n'; //显示
… //param
} //的类型
````
GNU和Clang执行这段代码将会输出这样的结果
````cpp
T = PK6Widget
param = PK6Widget
````
我们早就知道在这些编译器中`PK`表示“pointer to `const`”,所以只有数字`6`对我们来说是神奇的。其实数字是类名称(`Widget`)的字符串长度,所以这些编译器告诉我们`T`和`param`都是`const Widget*`。
Microsoft的编译器也同意上述言论:
````cpp
T = class Widget const *
param = class Widget const *
````
三个独立的编译器都产生了相同的信息,这表明信息应该是准确的。但仔细观察一下,在模板`f`中,`param`的声明类型是`const T&`。难道你们不觉得`T`和`param`类型相同很奇怪吗?比如`T`是`int`,`param`的类型应该是`const int&`而不是相同类型才对吧。
遗憾的是,事实就是这样,`std::type_info::name`的结果并不总是可信的,就像上面一样,三个编译器对`param`的报告都是错误的。此外,它们在本质上必须是这样的结果,因为`std::type_info::name`规范批准像传值形参一样来对待这些类型。正如[Item1](../1.DeducingTypes/item1.md)提到的,如果传递的是一个引用,那么引用部分(reference-ness)将被忽略,如果忽略后还具有`const`或者`volatile`,那么常量性`const`ness或者易变性`volatile`ness也会被忽略。那就是为什么`param`的类型`const Widget * const &`会输出为`const Widget *`,首先引用被忽略,然后这个指针自身的常量性`const`ness被忽略,剩下的就是指针指向一个常量对象。
同样遗憾的是,IDE编辑器显示的类型信息也不总是可靠的,或者说不总是有用的。还是一样的例子,一个IDE编辑器可能会把`T`的类型显示为(我没有胡编乱造):
````cpp
const
std::_Simple_types>::_Alloc>::value_type>::value_type *
````
同样把`param`的类型显示为
````cpp
const std::_Simple_types<...>::value_type *const &
````
这个比起`T`来说要简单一些,但是如果你不知道“`...`”表示编译器忽略`T`的部分类型那么可能你还是会产生困惑。如果你运气好点你的IDE可能表现得比这个要好一些。
比起运气如果你更倾向于依赖库,那么你会很乐意被告知,在`std::type_info::name`和IDE失效的地方,Boost TypeIndex库(通常写作**Boost.TypeIndex**)被设计成可以正常运作。这个库不是标准C++的一部分,也不是IDE或者`TD`这样的模板。Boost库(可在[boost.com](http://boost.org)获得)是跨平台,开源,有良好的开源协议的库,这意味着使用Boost和STL一样具有高度可移植性。
这里是如何使用Boost.TypeIndex得到`f`的类型的代码
````cpp
#include
template
void f(const T& param)
{
using std::cout;
using boost::typeindex::type_id_with_cvr;
//显示T
cout << "T = "
<< type_id_with_cvr().pretty_name()
<< '\n';
//显示param类型
cout << "param = "
<< type_id_with_cvr().pretty_name()
<< '\n';
}
````
`boost::typeindex::type_id_with_cvr`获取一个类型实参(我们想获得相应信息的那个类型),它不消除实参的`const`,`volatile`和引用修饰符(因此模板名中有“`with_cvr`”)。结果是一个`boost::typeindex::type_index`对象,它的`pretty_name`成员函数输出一个`std::string`,包含我们能看懂的类型表示。
基于这个`f`的实现版本,再次考虑那个使用`typeid`时获取`param`类型信息出错的调用:
````cpp
std::vetor createVec(); //工厂函数
const auto vw = createVec(); //使用工厂函数返回值初始化vw
if (!vw.empty()){
f(&vw[0]); //调用f
…
}
````
在GNU和Clang的编译器环境下,使用Boost.TypeIndex版本的`f`最后会产生下面的(准确的)输出:
````cpp
T = Widget const *
param = Widget const * const&
````
在Microsoft的编译器环境下,结果也是极其相似:
````cpp
T = class Widget const *
param = class Widget const * const &
````
这样近乎一致的结果是很不错的,但是请记住IDE,编译器错误诊断或者像Boost.TypeIndex这样的库只是用来帮助你理解编译器推导的类型是什么。它们是有用的,但是作为本章结束语我想说它们根本不能替代你对[Item1](../1.DeducingTypes/item1.md)-[3](../1.DeducingTypes/item3.md)提到的类型推导的理解。
**请记住:**
+ 类型推断可以从IDE看出,从编译器报错看出,从Boost TypeIndex库的使用看出
+ 这些工具可能既不准确也无帮助,所以理解C++类型推导规则才是最重要的
================================================
FILE: src/2.Auto/item5.md
================================================
# 第2章 `auto`
**CHAPTER 2 `auto`**
从概念上来说,`auto`要多简单有多简单,但是它比看起来要微妙一些。当然,使用它可以存储类型,但它也避免了困扰着手动声明类型的正确性和性能问题。此外,从程序员的角度来说,如果按照符合规定的流程走,那`auto`类型推导的一些结果是错误的。当这些情况发生时,对我们来说引导`auto`产生正确的结果是很重要的,因为回到手动声明类型是一种通常最好避免的替代方法。
本章简单的覆盖了`auto`的里里外外。
## 条款五:优先考虑`auto`而非显式类型声明
**Item 5: Prefer `auto` to explicit type declarations**
哈,开心一下:
````cpp
int x;
````
等等,该死!我忘记了初始化`x`,所以`x`的值是不确定的。它可能会被初始化为0,这得取决于工作环境。哎。
别介意,让我们转换一个话题, 对一个局部变量使用解引用迭代器的方式初始化:
````cpp
template //对从b到e的所有元素使用
void dwim(It b, It e) //dwim(“do what I mean”)算法
{
while (b != e) {
typename std::iterator_traits::value_type
currValue = *b;
…
}
}
````
嘿!`typename std::iterator_traits::value_type`是想表达迭代器指向的元素的值的类型吗?我无论如何都说不出它是多么有趣这样的话,该死!等等,我早就说过了吗?
好吧,声明一个局部变量,类型是一个闭包,闭包的类型只有编译器知道,因此我们写不出来,该死!
该死该死该死,C++编程不应该是这样不愉快的体验。
别担心,它只在过去是这样,到了C++11所有的这些问题都消失了,这都多亏了`auto`。`auto`变量从初始化表达式中推导出类型,所以我们必须初始化。这意味着当你在现代化C++的高速公路上飞奔的同时你不得不对只声明不初始化变量的老旧方法说拜拜:
````cpp
int x1; //潜在的未初始化的变量
auto x2; //错误!必须要初始化
auto x3 = 0; //没问题,x已经定义了
````
而且即使使用解引用迭代器初始化局部变量也不会对你的高速驾驶有任何影响
````cpp
template //如之前一样
void dwim(It b,It e)
{
while (b != e) {
auto currValue = *b;
…
}
}
````
因为使用[Item2](../1.DeducingTypes/item2.md)所述的`auto`类型推导技术,它甚至能表示一些只有编译器才知道的类型:
````cpp
auto derefUPLess =
[](const std::unique_ptr &p1, //用于std::unique_ptr
const std::unique_ptr &p2) //指向的Widget类型的
{ return *p1 < *p2; }; //比较函数
````
很酷对吧,如果使用C++14,将会变得更酷,因为*lambda*表达式中的形参也可以使用`auto`:
````cpp
auto derefLess = //C++14版本
[](const auto& p1, //被任何像指针一样的东西
const auto& p2) //指向的值的比较函数
{ return *p1 < *p2; };
````
尽管这很酷,但是你可能会想我们完全不需要使用`auto`声明局部变量来保存一个闭包,因为我们可以使用`std::function`对象。没错,我们的确可以那么做,但是事情可能不是完全如你想的那样。当然现在你可能会问,`std::function`对象到底是什么。让我来给你解释一下。
`std::function`是一个C++11标准模板库中的一个模板,它泛化了函数指针的概念。与函数指针只能指向函数不同,`std::function`可以指向任何可调用对象,也就是那些像函数一样能进行调用的东西。当你声明函数指针时你必须指定函数类型(即函数签名),同样当你创建`std::function`对象时你也需要提供函数签名,由于它是一个模板所以你需要在它的模板参数里面提供。举个例子,假设你想声明一个`std::function`对象`func`使它指向一个可调用对象,比如一个具有这样函数签名的函数,
````cpp
bool(const std::unique_ptr &, //C++11
const std::unique_ptr &) //std::unique_ptr
//比较函数的签名
````
你就得这么写:
````cpp
std::function &,
const std::unique_ptr &)> func;
````
因为*lambda*表达式能产生一个可调用对象,所以我们现在可以把闭包存放到`std::function`对象中。这意味着我们可以不使用`auto`写出C++11版的`derefUPLess`:
````cpp
std::function &,
const std::unique_ptr &)>
derefUPLess = [](const std::unique_ptr &p1,
const std::unique_ptr &p2)
{ return *p1 < *p2; };
````
语法冗长不说,还需要重复写很多形参类型,使用`std::function`还不如使用`auto`。用`auto`声明的变量保存一个和闭包一样类型的(新)闭包,因此使用了与闭包相同大小存储空间。实例化`std::function`并声明一个对象这个对象将会有固定的大小。这个大小可能不足以存储一个闭包,这个时候`std::function`的构造函数将会在堆上面分配内存来存储,这就造成了使用`std::function`比`auto`声明变量会消耗更多的内存。并且通过具体实现我们得知通过`std::function`调用一个闭包几乎无疑比`auto`声明的对象调用要慢。换句话说,`std::function`方法比`auto`方法要更耗空间且更慢,还可能有*out-of-memory*异常。并且正如上面的例子,比起写`std::function`实例化的类型来,使用`auto`要方便得多。在这场存储闭包的比赛中,`auto`无疑取得了胜利(也可以使用`std::bind`来生成一个闭包,但在[Item34](../6.LambdaExpressions/item34.md)我会尽我最大努力说服你使用*lambda*表达式代替`std::bind`)
使用`auto`除了可以避免未初始化的无效变量,省略冗长的声明类型,直接保存闭包外,它还有一个好处是可以避免一个问题,我称之为与类型快捷方式(type shortcuts)有关的问题。你将看到这样的代码——甚至你会这么写:
````cpp
std::vector v;
…
unsigned sz = v.size();
````
`v.size()`的标准返回类型是`std::vector::size_type`,但是只有少数开发者意识到这点。`std::vector::size_type`实际上被指定为无符号整型,所以很多人都认为用`unsigned`就足够了,写下了上述的代码。这会造成一些有趣的结果。举个例子,在**Windows 32-bit**上`std::vector::size_type`和`unsigned`是一样的大小,但是在**Windows 64-bit**上`std::vector::size_type`是64位,`unsigned`是32位。这意味着这段代码在Windows 32-bit上正常工作,但是当把应用程序移植到Windows 64-bit上时就可能会出现一些问题。谁愿意花时间处理这些细枝末节的问题呢?
所以使用`auto`可以确保你不需要浪费时间:
````cpp
auto sz =v.size(); //sz的类型是std::vector::size_type
````
你还是不相信使用`auto`是多么明智的选择?考虑下面的代码:
````cpp
std::unordered_map m;
…
for(const std::pair& p : m)
{
… //用p做一些事
}
````
看起来好像很合情合理的表达,但是这里有一个问题,你看到了吗?
要想看到错误你就得知道`std::unordered_map`的*key*是`const`的,所以*hash table*(`std::unordered_map`本质上的东西)中的`std::pair`的类型不是`std::pair`,而是`std::pair`。但那不是在循环中的变量`p`声明的类型。编译器会努力的找到一种方法把`std::pair`(即*hash table*中的东西)转换为`std::pair`(`p`的声明类型)。它会成功的,因为它会通过拷贝`m`中的对象创建一个临时对象,这个临时对象的类型是`p`想绑定到的对象的类型,即`m`中元素的类型,然后把`p`的引用绑定到这个临时对象上。在每个循环迭代结束时,临时对象将会销毁,如果你写了这样的一个循环,你可能会对它的一些行为感到非常惊讶,因为你确信你只是让成为`p`指向`m`中各个元素的引用而已。
使用`auto`可以避免这些很难被意识到的类型不匹配的错误:
````cpp
for(const auto& p : m)
{
… //如之前一样
}
````
这样无疑更具效率,且更容易书写。而且,这个代码有一个非常吸引人的特性,如果你获取`p`的地址,你确实会得到一个指向`m`中元素的指针。在没有`auto`的版本中`p`会指向一个临时变量,这个临时变量在每次迭代完成时会被销毁。
前面这两个例子——应当写`std::vector::size_type`时写了`unsigned`,应当写`std::pair`时写了`std::pair`——说明了显式的指定类型可能会导致你不想看到的类型转换。如果你使用`auto`声明目标变量你就不必担心这个问题。
基于这些原因我建议你优先考虑`auto`而非显式类型声明。然而`auto`也不是完美的。每个`auto`变量都从初始化表达式中推导类型,有一些表达式的类型和我们期望的大相径庭。关于在哪些情况下会发生这些问题,以及你可以怎么解决这些问题我们在[Item2](../1.DeducingTypes/item2.md)和[6](../2.Auto/item6.md)讨论,所以这里我不再赘述。我想把注意力放到你可能关心的另一点:使用auto代替传统类型声明对源码可读性的影响。
首先,深呼吸,放松,`auto`是**可选项**,不是**命令**,在某些情况下如果你的专业判断告诉你使用显式类型声明比`auto`要更清晰更易维护,那你就不必再坚持使用`auto`。但是要牢记,C++没有在其他众所周知的语言所拥有的类型推导(*type inference*)上开辟新土地。其他静态类型的过程式语言(如C#、D、Sacla、Visual Basic)或多或少都有等价的特性,更不必提那些静态类型的函数式语言了(如ML、Haskell、OCaml、F#等)。在某种程度上,这是因为动态类型语言,如Perl、Python、Ruby等的成功;在这些语言中,几乎没有显式的类型声明。软件开发社区对于类型推导有丰富的经验,他们展示了在维护大型工业强度的代码上使用这种技术没有任何争议。
一些开发者也担心使用`auto`就不能瞥一眼源代码便知道对象的类型,然而,IDE扛起了部分担子(也考虑到了[Item4](../1.DeducingTypes/item4.md)中提到的IDE类型显示问题),在很多情况下,少量显示一个对象的类型对于知道对象的确切类型是有帮助的,这通常已经足够了。举个例子,要想知道一个对象是容器还是计数器还是智能指针,不需要知道它的确切类型。一个适当的变量名称就能告诉我们大量的抽象类型信息。
事实是显式指定类型通常只会引入一些微妙的错误,无论是在正确性还是效率方面。而且,如果初始化表达式的类型改变,则`auto`推导出的类型也会改变,这意味着使用`auto`可以帮助我们完成一些重构工作。举个例子,如果一个函数返回类型被声明为`int`,但是后来你认为将它声明为`long`会更好,调用它作为初始化表达式的变量会自动改变类型,但是如果你不使用`auto`你就不得不在源代码中挨个找到调用地点然后修改它们。
**请记住:**
+ `auto`变量必须初始化,通常它可以避免一些移植性和效率性的问题,也使得重构更方便,还能让你少打几个字。
+ 正如[Item2](../1.DeducingTypes/item2.md)和[6](../2.Auto/item6.md)讨论的,`auto`类型的变量可能会踩到一些陷阱。
================================================
FILE: src/2.Auto/item6.md
================================================
## 条款六:`auto`推导若非己愿,使用显式类型初始化惯用法
**Item 6: Use the explicitly typed initializer idiom when `auto` deduces undesired types**
在[Item5](../2.Auto/item5.md)中解释了比起显式指定类型使用`auto`声明变量有若干技术优势,但是有时当你想向左转`auto`却向右转。举个例子,假如我有一个函数,参数为`Widget`,返回一个`std::vector`,这里的`bool`表示`Widget`是否提供一个独有的特性。
````cpp
std::vector features(const Widget& w);
````
更进一步假设第5个*bit*表示`Widget`是否具有高优先级,我们可以写这样的代码:
````cpp
Widget w;
…
bool highPriority = features(w)[5]; //w高优先级吗?
…
processWidget(w, highPriority); //根据它的优先级处理w
````
这个代码没有任何问题。它会正常工作,但是如果我们使用`auto`代替`highPriority`的显式指定类型做一些看起来很无害的改变:
````cpp
auto highPriority = features(w)[5]; //w高优先级吗?
````
情况变了。所有代码仍然可编译,但是行为不再可预测:
````cpp
processWidget(w,highPriority); //未定义行为!
````
就像注释说的,这个`processWidget`是一个未定义行为。为什么呢?答案有可能让你很惊讶,使用`auto`后`highPriority`不再是`bool`类型。虽然从概念上来说`std::vector`意味着存放`bool`,但是`std::vector`的`operator[]`不会返回容器中元素的引用(这就是`std::vector::operator[]`可返回**除了`bool`以外**的任何类型),取而代之它返回一个`std::vector::reference`的对象(一个嵌套于`std::vector`中的类)。
`std::vector::reference`之所以存在是因为`std::vector`规定了使用一个打包形式(packed form)表示它的`bool`,每个`bool`占一个*bit*。那给`std::vector`的`operator[]`带来了问题,因为`std::vector`的`operator[]`应当返回一个`T&`,但是C++禁止对`bit`s的引用。无法返回一个`bool&`,`std::vector`的`operator[]`返回一个**行为类似于**`bool&`的对象。要想成功扮演这个角色,`bool&`适用的上下文`std::vector::reference`也必须一样能适用。在`std::vector::reference`的特性中,使这个原则可行的特性是一个可以向`bool`的隐式转化。(不是`bool&`,是**`bool`**。要想完整的解释`std::vector::reference`能模拟`bool&`的行为所使用的一堆技术可能扯得太远了,所以这里简单地说隐式类型转换只是这个大型马赛克的一小块)
有了这些信息,我们再来看看原始代码的一部分:
````cpp
bool highPriority = features(w)[5]; //显式的声明highPriority的类型
````
这里,`features`返回一个`std::vector`对象后再调用`operator[]`,`operator[]`将会返回一个`std::vector::reference`对象,然后再通过隐式转换赋值给`bool`变量`highPriority`。`highPriority`因此表示的是`features`返回的`std::vector`中的第五个*bit*,这也正如我们所期待的那样。
然后再对照一下当使用`auto`时发生了什么:
````cpp
auto highPriority = features(w)[5]; //推导highPriority的类型
````
同样的,`features`返回一个`std::vector`对象,再调用`operator[]`,`operator[]`将会返回一个`std::vector::reference`对象,但是现在这里有一点变化了,`auto`推导`highPriority`的类型为`std::vector::reference`,但是`highPriority`对象没有第五*bit*的值。
这个值取决于`std::vector::reference`的具体实现。其中的一种实现是这样的(`std::vector::reference`)对象包含一个指向机器字(*word*)的指针,然后加上方括号中的偏移实现被引用*bit*这样的行为。然后再来考虑`highPriority`初始化表达的意思,注意这里假设`std::vector::reference`就是刚提到的实现方式。
调用`features`将返回一个`std::vector`临时对象,这个对象没有名字,为了方便我们的讨论,我这里叫他`temp`。`operator[]`在`temp`上调用,它返回的`std::vector::reference`包含一个指向存着这些*bit*s的一个数据结构中的一个*word*的指针(`temp`管理这些*bit*s),还有相应于第5个*bit*的偏移。`highPriority`是这个`std::vector::reference`的拷贝,所以`highPriority`也包含一个指针,指向`temp`中的这个*word*,加上相应于第5个*bit*的偏移。在这个语句结束的时候`temp`将会被销毁,因为它是一个临时变量。因此`highPriority`包含一个悬置的(*dangling*)指针,如果用于`processWidget`调用中将会造成未定义行为:
````cpp
processWidget(w, highPriority); //未定义行为!
//highPriority包含一个悬置指针!
````
`std::vector::reference`是一个代理类(*proxy class*)的例子:所谓代理类就是以模仿和增强一些类型的行为为目的而存在的类。很多情况下都会使用代理类,`std::vector::reference`展示了对`std::vector`使用`operator[]`来实现引用*bit*这样的行为。另外,C++标准模板库中的智能指针(见[第4章](../4.SmartPointers/item18.md))也是用代理类实现了对原始指针的资源管理行为。代理类的功能已被大家广泛接受。事实上,“Proxy”设计模式是软件设计这座万神庙中一直都存在的高级会员。
一些代理类被设计于用以对客户可见。比如`std::shared_ptr`和`std::unique_ptr`。其他的代理类则或多或少不可见,比如`std::vector::reference`就是不可见代理类的一个例子,还有它在`std::bitset`的胞弟`std::bitset::reference`。
在后者的阵营(注:指不可见代理类)里一些C++库也是用了表达式模板(*expression templates*)的黑科技。这些库通常被用于提高数值运算的效率。给出一个矩阵类`Matrix`和矩阵对象`m1`,`m2`,`m3`,`m4`,举个例子,这个表达式
````cpp
Matrix sum = m1 + m2 + m3 + m4;
````
可以使计算更加高效,只需要使让`operator+`返回一个代理类代理结果而不是返回结果本身。也就是说,对两个`Matrix`对象使用`operator+`将会返回如`Sum`这样的代理类作为结果而不是直接返回一个`Matrix`对象。在`std::vector::reference`和`bool`中存在一个隐式转换,同样对于`Matrix`来说也可以存在一个隐式转换允许`Matrix`的代理类转换为`Matrix`,这让表达式等号“`=`”右边能产生代理对象来初始化`sum`。(这个对象应当编码整个初始化表达式,即类似于`Sum, Matrix>, Matrix>`的东西。客户应该避免看到这个实际的类型。)
作为一个通则,不可见的代理类通常不适用于`auto`。这样类型的对象的生命期通常不会设计为能活过一条语句,所以创建那样的对象你基本上就走向了违反程序库设计基本假设的道路。`std::vector::reference`就是这种情况,我们看到违反这个基本假设将导致未定义行为。
因此你想避开这种形式的代码:
````cpp
auto someVar = expression of "invisible" proxy class type;
````
但是你怎么能意识到你正在使用代理类?应用他们的软件不可能宣告它们的存在。它们被设计为**不可见**,至少概念上说是这样!每当你发现它们,你真的应该舍弃[Item5](../2.Auto/item5.md)演示的`auto`所具有的诸多好处吗?
让我们首先回到如何找到它们的问题上。虽然“不可见”代理类都在程序员日常使用的雷达下方飞行,但是很多库都证明它们可以上方飞行。当你越熟悉你使用的库的基本设计理念,你的思维就会越活跃,不至于思维僵化认为代理类只能在这些库中使用。
当缺少文档的时候,可以去看看头文件。很少会出现源代码全都用代理对象,它们通常用于一些函数的返回类型,所以通常能从函数签名中看出它们的存在。这里有一份`std::vector::operator[]`的说明书:
````cpp
namespace std{ //来自于C++标准库
template
class vector{
public:
…
class reference { … };
reference operator[](size_type n);
…
};
}
````
假设你知道对`std::vector`使用`operator[]`通常会返回一个`T&`,在这里`operator[]`不寻常的返回类型提示你它使用了代理类。多关注你使用的接口可以暴露代理类的存在。
实际上, 很多开发者都是在跟踪一些令人困惑的复杂问题或在单元测试出错进行调试时才看到代理类的使用。不管你怎么发现它们的,一旦看到`auto`推导了代理类的类型而不是被代理的类型,解决方案并不需要抛弃`auto`。`auto`本身没什么问题,问题是`auto`不会推导出你想要的类型。解决方案是强制使用一个不同的类型推导形式,这种方法我通常称之为显式类型初始器惯用法(*the explicitly typed initialized idiom*)。
显式类型初始器惯用法使用`auto`声明一个变量,然后对表达式强制类型转换(*cast*)得出你期望的推导结果。举个例子,我们该怎么将这个惯用法施加到`highPriority`上?
````cpp
auto highPriority = static_cast(features(w)[5]);
````
这里,`features(w)[5]`还是返回一个`std::vector::reference`对象,就像之前那样,但是这个转型使得表达式类型为`bool`,然后`auto`才被用于推导`highPriority`。在运行时,对`std::vector::operator[]`返回的`std::vector::reference`执行它支持的向`bool`的转型,在这个过程中指向`std::vector`的指针已经被解引用。这就避开了我们之前的未定义行为。然后5将被用于指向*bit*的指针,`bool`值被用于初始化`highPriority`。
对于`Matrix`来说,显式类型初始器惯用法是这样的:
````cpp
auto sum = static_cast(m1 + m2 + m3 + m4);
````
应用这个惯用法不限制初始化表达式产生一个代理类。它也可以用于强调你声明了一个变量类型,它的类型不同于初始化表达式的类型。举个例子,假设你有这样一个表达式计算公差值:
````cpp
double calcEpsilon(); //返回公差值
````
`calcEpsilon`清楚的表明它返回一个`double`,但是假设你知道对于这个程序来说使用`float`的精度已经足够了,而且你很关心`double`和`float`的大小。你可以声明一个`float`变量储存`calEpsilon`的计算结果。
````cpp
float ep = calcEpsilon(); //double到float隐式转换
````
但是这几乎没有表明“我确实要减少函数返回值的精度”。使用显式类型初始器惯用法我们可以这样:
````cpp
auto ep = static_cast(calcEpsilon());
````
出于同样的原因,如果你故意想用整数类型存储一个表达式返回的浮点数类型的结果,你也可以使用这个方法。假如你需要计算一个随机访问迭代器(比如`std::vector`,`std::deque`或者`std::array`)中某元素的下标,你被提供一个`0.0`到`1.0`的`double`值表明这个元素离容器的头部有多远(`0.5`意味着位于容器中间)。进一步假设你很自信结果下标是`int`。如果容器是`c`,`d`是`double`类型变量,你可以用这样的方法计算容器下标:
````cpp
int index = d * c.size();
````
但是这种写法并没有明确表明你想将右侧的`double`类型转换成`int`类型,显式类型初始器可以帮助你正确表意:
````cpp
auto index = static_cast(d * size());
````
**请记住:**
+ 不可见的代理类可能会使`auto`从表达式中推导出“错误的”类型
+ 显式类型初始器惯用法强制`auto`推导出你想要的结果
================================================
FILE: src/3.MovingToModernCpp/item10.md
================================================
## 条款十:优先考虑限域`enum`而非未限域`enum`
**Item 10: Prefer scoped `enum`s to unscoped `enum`s**
通常来说,在花括号中声明一个名字会限制它的作用域在花括号之内。但这对于C++98风格的`enum`中声明的枚举名(译注:*enumerator*,连同下文“枚举名”都指*enumerator*)是不成立的。这些枚举名的名字(译注:*enumerator* names,连同下文“名字”都指names)属于包含这个`enum`的作用域,这意味着作用域内不能含有相同名字的其他东西:
```cpp
enum Color { black, white, red }; //black, white, red在
//Color所在的作用域
auto white = false; //错误! white早已在这个作用
//域中声明
```
这些枚举名的名字泄漏进它们所被定义的`enum`在的那个作用域,这个事实有一个官方的术语:未限域枚举(*unscoped `enum`*)。在C++11中它们有一个相似物,限域枚举(*scoped `enum`*),它不会导致枚举名泄漏:
```cpp
enum class Color { black, white, red }; //black, white, red
//限制在Color域内
auto white = false; //没问题,域内没有其他“white”
Color c = white; //错误,域中没有枚举名叫white
Color c = Color::white; //没问题
auto c = Color::white; //也没问题(也符合Item5的建议)
```
因为限域`enum`是通过“`enum class`”声明,所以它们有时候也被称为枚举类(*`enum` classes*)。
使用限域`enum`来减少命名空间污染,这是一个足够合理使用它而不是它的同胞未限域`enum`的理由,其实限域`enum`还有第二个吸引人的优点:在它的作用域中,枚举名是强类型。未限域`enum`中的枚举名会隐式转换为整型(现在,也可以转换为浮点类型)。因此下面这种歪曲语义的做法也是完全有效的:
```cpp
enum Color { black, white, red }; //未限域enum
std::vector //func返回x的质因子
primeFactors(std::size_t x);
Color c = red;
…
if (c < 14.5) { // Color与double比较 (!)
auto factors = // 计算一个Color的质因子(!)
primeFactors(c);
…
}
```
在`enum`后面写一个`class`就可以将非限域`enum`转换为限域`enum`,接下来就是完全不同的故事展开了。现在不存在任何隐式转换可以将限域`enum`中的枚举名转化为任何其他类型:
```cpp
enum class Color { black, white, red }; //Color现在是限域enum
Color c = Color::red; //和之前一样,只是
... //多了一个域修饰符
if (c < 14.5) { //错误!不能比较
//Color和double
auto factors = //错误!不能向参数为std::size_t
primeFactors(c); //的函数传递Color参数
…
}
```
如果你真的很想执行`Color`到其他类型的转换,和平常一样,使用正确的类型转换运算符扭曲类型系统:
```cpp
if (static_cast(c) < 14.5) { //奇怪的代码,
//但是有效
auto factors = //有问题,但是
primeFactors(static_cast(c)); //能通过编译
…
}
```
似乎比起非限域`enum`而言,限域`enum`有第三个好处,因为限域`enum`可以被前置声明。也就是说,它们可以不指定枚举名直接声明:
```cpp
enum Color; //错误!
enum class Color; //没问题
```
其实这是一个误导。在C++11中,非限域`enum`也可以被前置声明,但是只有在做一些其他工作后才能实现。这些工作来源于一个事实:在C++中所有的`enum`都有一个由编译器决定的整型的底层类型。对于非限域`enum`比如`Color`,
```cpp
enum Color { black, white, red };
```
编译器可能选择`char`作为底层类型,因为这里只需要表示三个值。然而,有些`enum`中的枚举值范围可能会大些,比如:
```cpp
enum Status { good = 0,
failed = 1,
incomplete = 100,
corrupt = 200,
indeterminate = 0xFFFFFFFF
};
```
这里值的范围从`0`到`0xFFFFFFFF`。除了在不寻常的机器上(比如一个`char`至少有32bits的那种),编译器都会选择一个比`char`大的整型类型来表示`Status`。
为了高效使用内存,编译器通常在确保能包含所有枚举值的前提下为`enum`选择一个最小的底层类型。在一些情况下,编译器将会优化速度,舍弃大小,这种情况下它可能不会选择最小的底层类型,但它们当然希望能够针对大小进行优化。为此,C++98只支持`enum`定义(所有枚举名全部列出来);`enum`声明是不被允许的。这使得编译器能在使用之前为每一个`enum`选择一个底层类型。
但是不能前置声明`enum`也是有缺点的。最大的缺点莫过于它可能增加编译依赖。再次考虑`Status` `enum`:
```cpp
enum Status { good = 0,
failed = 1,
incomplete = 100,
corrupt = 200,
indeterminate = 0xFFFFFFFF
};
```
这种`enum`很有可能用于整个系统,因此系统中每个包含这个头文件的组件都会依赖它。如果引入一个新状态值,
```cpp
enum Status { good = 0,
failed = 1,
incomplete = 100,
corrupt = 200,
audited = 500,
indeterminate = 0xFFFFFFFF
};
```
那么可能整个系统都得重新编译,即使只有一个子系统——或者只有一个函数——使用了新添加的枚举名。这是大家都**不希望**看到的。C++11中的前置声明`enum`s可以解决这个问题。比如这里有一个完全有效的限域`enum`声明和一个以该限域`enum`作为形参的函数声明:
```cpp
enum class Status; //前置声明
void continueProcessing(Status s); //使用前置声明enum
```
即使`Status`的定义发生改变,包含这些声明的头文件也不需要重新编译。而且如果`Status`有改动(比如添加一个`audited`枚举名),`continueProcessing`的行为不受影响(比如因为`continueProcessing`没有使用这个新添加的`audited`),`continueProcessing`也不需要重新编译。
但是如果编译器在使用它之前需要知晓该`enum`的大小,该怎么声明才能让C++11做到C++98不能做到的事情呢?答案很简单:限域`enum`的底层类型总是已知的,而对于非限域`enum`,你可以指定它。
默认情况下,限域枚举的底层类型是`int`:
```cpp
enum class Status; //底层类型是int
```
如果默认的`int`不适用,你可以重写它:
```cpp
enum class Status: std::uint32_t; //Status的底层类型
//是std::uint32_t
//(需要包含 )
```
不管怎样,编译器都知道限域`enum`中的枚举名占用多少字节。
要为非限域`enum`指定底层类型,你可以同上,结果就可以前向声明:
```cpp
enum Color: std::uint8_t; //非限域enum前向声明
//底层类型为
//std::uint8_t
```
底层类型说明也可以放到`enum`定义处:
```cpp
enum class Status: std::uint32_t { good = 0,
failed = 1,
incomplete = 100,
corrupt = 200,
audited = 500,
indeterminate = 0xFFFFFFFF
};
```
限域`enum`避免命名空间污染而且不接受荒谬的隐式类型转换,但它并非万事皆宜,你可能会很惊讶听到至少有一种情况下非限域`enum`是很有用的。那就是牵扯到C++11的`std::tuple`的时候。比如在社交网站中,假设我们有一个*tuple*保存了用户的名字,email地址,声望值:
```cpp
using UserInfo = //类型别名,参见Item9
std::tuple ; //声望
```
虽然注释说明了tuple各个字段对应的意思,但当你在另一文件遇到下面的代码那之前的注释就不是那么有用了:
```cpp
UserInfo uInfo; //tuple对象
…
auto val = std::get<1>(uInfo); //获取第一个字段
```
作为一个程序员,你有很多工作要持续跟进。你应该记住第一个字段代表用户的email地址吗?我认为不。可以使用非限域`enum`将名字和字段编号关联起来以避免上述需求:
```cpp
enum UserInfoFields { uiName, uiEmail, uiReputation };
UserInfo uInfo; //同之前一样
…
auto val = std::get(uInfo); //啊,获取用户email字段的值
```
之所以它能正常工作是因为`UserInfoFields`中的枚举名隐式转换成`std::size_t`了,其中`std::size_t`是`std::get`模板实参所需的。
对应的限域`enum`版本就很啰嗦了:
```cpp
enum class UserInfoFields { uiName, uiEmail, uiReputation };
UserInfo uInfo; //同之前一样
…
auto val =
std::get(UserInfoFields::uiEmail)>
(uInfo);
```
为避免这种冗长的表示,我们可以写一个函数传入枚举名并返回对应的`std::size_t`值,但这有一点技巧性。`std::get`是一个模板(函数),需要你给出一个`std::size_t`值的模板实参(注意使用`<>`而不是`()`),因此将枚举名变换为`std::size_t`值的函数必须**在编译期**产生这个结果。如[Item15](../3.MovingToModernCpp/item15.md)提到的,那必须是一个`constexpr`函数。
事实上,它也的确该是一个`constexpr`函数模板,因为它应该能用于任何`enum`。如果我们想让它更一般化,我们还要泛化它的返回类型。较之于返回`std::size_t`,我们更应该返回枚举的底层类型。这可以通过`std::underlying_type`这个*type trait*获得。(参见[Item9](../3.MovingToModernCpp/item9.md)关于*type trait*的内容)。最终我们还要再加上`noexcept`修饰(参见[Item14](../3.MovingToModernCpp/item14.md)),因为我们知道它肯定不会产生异常。根据上述分析最终得到的`toUType`函数模板在编译期接受任意枚举名并返回它的值:
```cpp
template
constexpr typename std::underlying_type::type
toUType(E enumerator) noexcept
{
return
static_cast::type>(enumerator);
}
```
在C++14中,`toUType`还可以进一步用`std::underlying_type_t`(参见[Item9](../3.MovingToModernCpp/item9.md))代替`typename std::underlying_type::type`打磨:
```cpp
template //C++14
constexpr std::underlying_type_t
toUType(E enumerator) noexcept
{
return static_cast>(enumerator);
}
```
还可以再用C++14 `auto`(参见[Item3](../1.DeducingTypes/item3.md))打磨一下代码:
```cpp
template //C++14
constexpr auto
toUType(E enumerator) noexcept
{
return static_cast>(enumerator);
}
```
不管它怎么写,`toUType`现在允许这样访问tuple的字段了:
```cpp
auto val = std::get(uInfo);
```
这仍然比使用非限域`enum`要写更多的代码,但同时它也避免命名空间污染,防止不经意间使用隐式转换。大多数情况下,你应该会觉得多敲几个(几行)字符作为避免使用未限域枚举这种老得和2400波特率猫同时代技术的代价是值得的。
**记住**
+ C++98的`enum`即非限域`enum`。
+ 限域`enum`的枚举名仅在`enum`内可见。要转换为其它类型只能使用*cast*。
+ 非限域/限域`enum`都支持底层类型说明语法,限域`enum`底层类型默认是`int`。非限域`enum`没有默认底层类型。
+ 限域`enum`总是可以前置声明。非限域`enum`仅当指定它们的底层类型时才能前置。
================================================
FILE: src/3.MovingToModernCpp/item11.md
================================================
## 条款十一:优先考虑使用*deleted*函数而非使用未定义的私有声明
**Item 11: Prefer deleted functions to private undefined ones.**
如果你写的代码要被其他人使用,你不想让他们调用某个特殊的函数,你通常不会声明这个函数。无声明,不函数。简简单单!但有时C++会给你自动声明一些函数,如果你想防止客户调用这些函数,事情就不那么简单了。
上述场景见于特殊的成员函数,即当有必要时C++自动生成的那些函数。[Item17](../3.MovingToModernCpp/item17.md)详细讨论了这些函数,但是现在,我们只关心拷贝构造函数和拷贝赋值运算符重载。本节主要致力于讨论C++98中那些被C++11所取代的最佳实践,而且在C++98中,你想要禁止使用的成员函数,几乎总是拷贝构造函数或者赋值运算符,或者两者都是。
在C++98中防止调用这些函数的方法是将它们声明为私有(`private`)成员函数并且不定义。举个例子,在C++ 标准库*iostream*继承链的顶部是模板类`basic_ios`。所有*istream*和*ostream*类都继承此类(直接或者间接)。拷贝*istream*和*ostream*是不合适的,因为这些操作应该怎么做是模棱两可的。比如一个`istream`对象,代表一个输入值的流,流中有一些已经被读取,有一些可能马上要被读取。如果一个*istream*被拷贝,需要拷贝将要被读取的值和已经被读取的值吗?解决这个问题最好的方法是不定义这个操作。直接禁止拷贝流。
要使这些*istream*和*ostream*类不可拷贝,`basic_ios`在C++98中是这样声明的(包括注释):
```cpp
template >
class basic_ios : public ios_base {
public:
…
private:
basic_ios(const basic_ios& ); // not defined
basic_ios& operator=(const basic_ios&); // not defined
};
```
将它们声明为私有成员可以防止客户端调用这些函数。故意不定义它们意味着假如还是有代码用它们(比如成员函数或者类的友元`friend`),就会在链接时引发缺少函数定义(*missing function definitions*)错误。
在C++11中有一种更好的方式达到相同目的:用“`= delete`”将拷贝构造函数和拷贝赋值运算符标记为***deleted*函数**(译注:一些文献翻译为“删除的函数”)。上面相同的代码在C++11中是这样声明的:
```cpp
template >
class basic_ios : public ios_base {
public:
…
basic_ios(const basic_ios& ) = delete;
basic_ios& operator=(const basic_ios&) = delete;
…
};
```
删除这些函数(译注:添加"`= delete`")和声明为私有成员可能看起来只是方式不同,别无其他区别。其实还有一些实质性意义。*deleted*函数不能以任何方式被调用,即使你在成员函数或者友元函数里面调用*deleted*函数也不能通过编译。这是较之C++98行为的一个改进,C++98中不正确的使用这些函数在链接时才被诊断出来。
通常,*deleted*函数被声明为`public`而不是`private`。这也是有原因的。当客户端代码试图调用成员函数,C++会在检查*deleted*状态前检查它的访问性。当客户端代码调用一个私有的*deleted*函数,一些编译器只会给出该函数是`private`的错误(译注:而没有诸如该函数被*deleted*修饰的错误),即使函数的访问性不影响它是否能被使用。所以值得牢记,如果要将老代码的“私有且未定义”函数替换为*deleted*函数时请一并修改它的访问性为`public`,这样可以让编译器产生更好的错误信息。
*deleted*函数还有一个重要的优势是**任何**函数都可以标记为*deleted*,而只有成员函数可被标记为`private`。(译注:从下文可知“任何”是包含普通函数和成员函数等所有可声明函数的地方,而`private`方法只适用于成员函数)假如我们有一个非成员函数,它接受一个整型参数,检查它是否为幸运数:
```cpp
bool isLucky(int number);
```
C++有沉重的C包袱,使得含糊的、能被视作数值的任何类型都能隐式转换为`int`,但是有一些调用可能是没有意义的:
```cpp
if (isLucky('a')) … //字符'a'是幸运数?
if (isLucky(true)) … //"true"是?
if (isLucky(3.5)) … //难道判断它的幸运之前还要先截尾成3?
```
如果幸运数必须真的是整型,我们该禁止这些调用通过编译。
其中一种方法就是创建*deleted*重载函数,其参数就是我们想要过滤的类型:
```cpp
bool isLucky(int number); //原始版本
bool isLucky(char) = delete; //拒绝char
bool isLucky(bool) = delete; //拒绝bool
bool isLucky(double) = delete; //拒绝float和double
```
(上面`double`重载版本的注释说拒绝`float`和`double`可能会让你惊讶,但是请回想一下:将`float`转换为`int`和`double`,C++更喜欢转换为`double`。使用`float`调用`isLucky`因此会调用`double`重载版本,而不是`int`版本。好吧,它也会那么去尝试。事实是调用被删除的`double`重载版本不能通过编译。不再惊讶了吧。)
虽然*deleted*函数不能被使用,但它们还是存在于你的程序中。也即是说,重载决议会考虑它们。这也是为什么上面的函数声明导致编译器拒绝一些不合适的函数调用。
```cpp
if (isLucky('a')) … //错误!调用deleted函数
if (isLucky(true)) … //错误!
if (isLucky(3.5f)) … //错误!
```
另一个*deleted*函数用武之地(`private`成员函数做不到的地方)是禁止一些模板的实例化。假如你要求一个模板仅支持原生指针(尽管[第四章](../4.SmartPointers/item18.md)建议使用智能指针代替原生指针):
```cpp
template
void processPointer(T* ptr);
```
在指针的世界里有两种特殊情况。一是`void*`指针,因为没办法对它们进行解引用,或者加加减减等。另一种指针是`char*`,因为它们通常代表C风格的字符串,而不是正常意义下指向单个字符的指针。这两种情况要特殊处理,在`processPointer`模板里面,我们假设正确的函数应该拒绝这些类型。也即是说,`processPointer`不能被`void*`和`char*`调用。
要想确保这个很容易,使用`delete`标注模板实例:
```cpp
template<>
void processPointer(void*) = delete;
template<>
void processPointer(char*) = delete;
```
现在如果使用`void*`和`char*`调用`processPointer`就是无效的,按常理说`const void*`和`const char*`也应该无效,所以这些实例也应该标注`delete`:
```cpp
template<>
void processPointer(const void*) = delete;
template<>
void processPointer(const char*) = delete;
```
如果你想做得更彻底一些,你还要删除`const volatile void*`和`const volatile char*`重载版本,另外还需要一并删除其他标准字符类型的重载版本:`std::wchar_t`,`std::char16_t`和`std::char32_t`。
有趣的是,如果类里面有一个函数模板,你可能想用`private`(经典的C++98惯例)来禁止这些函数模板实例化,但是不能这样做,因为不能给特化的成员模板函数指定一个不同于主函数模板的访问级别。如果`processPointer`是类`Widget`里面的模板函数, 你想禁止它接受`void*`参数,那么通过下面这样C++98的方法就不能通过编译:
```cpp
class Widget {
public:
…
template
void processPointer(T* ptr)
{ … }
private:
template<> //错误!
void processPointer(void*);
};
```
问题是模板特例化必须位于一个命名空间作用域,而不是类作用域。*deleted*函数不会出现这个问题,因为它不需要一个不同的访问级别,且他们可以在类外被删除(因此位于命名空间作用域):
```cpp
class Widget {
public:
…
template
void processPointer(T* ptr)
{ … }
…
};
template<> //还是public,
void Widget::processPointer(void*) = delete; //但是已经被删除了
```
事实上C++98的最佳实践即声明函数为`private`但不定义是在做C++11 *deleted*函数要做的事情。作为模仿者,C++98的方法不是十全十美。它不能在类外正常工作,不能总是在类中正常工作,它的罢工可能直到链接时才会表现出来。所以请坚定不移的使用*deleted*函数。
**请记住:**
+ 比起声明函数为`private`但不定义,使用*deleted*函数更好
+ 任何函数都能被删除(be deleted),包括非成员函数和模板实例(译注:实例化的函数)
================================================
FILE: src/3.MovingToModernCpp/item12.md
================================================
## 条款十二:使用`override`声明重写函数
**Item 12: Declare overriding functions `override`**
在C++面向对象的世界里,涉及的概念有类,继承,虚函数。这个世界最基本的概念是派生类的虚函数**重写**基类同名函数。令人遗憾的是虚函数重写可能一不小心就错了。似乎这部分语言的设计理念是不仅仅要遵守墨菲定律,还应该尊重它。
虽然“重写(*overriding*)”听起来像“重载(*overloading*)”,然而两者完全不相关,所以让我澄清一下,正是虚函数重写机制的存在,才使我们可以通过基类的接口调用派生类的成员函数:
```cpp
class Base {
public:
virtual void doWork(); //基类虚函数
…
};
class Derived: public Base {
public:
virtual void doWork(); //重写Base::doWork
… //(这里“virtual”是可以省略的)
};
std::unique_ptr upb = //创建基类指针指向派生类对象
std::make_unique(); //关于std::make_unique
… //请参见Item21
upb->doWork(); //通过基类指针调用doWork,
//实际上是派生类的doWork
//函数被调用
```
要想重写一个函数,必须满足下列要求:
+ 基类函数必须是`virtual`
+ 基类和派生类函数名必须完全一样(除非是析构函数)
+ 基类和派生类函数形参类型必须完全一样
+ 基类和派生类函数常量性`const`ness必须完全一样
+ 基类和派生类函数的返回值和异常说明(*exception specifications*)必须兼容
除了这些C++98就存在的约束外,C++11又添加了一个:
+ 函数的引用限定符(*reference qualifiers*)必须完全一样。成员函数的引用限定符是C++11很少抛头露脸的特性,所以如果你从没听过它无需惊讶。它可以限定成员函数只能用于左值或者右值。成员函数不需要`virtual`也能使用它们:
```cpp
class Widget {
public:
…
void doWork() &; //只有*this为左值的时候才能被调用
void doWork() &&; //只有*this为右值的时候才能被调用
};
…
Widget makeWidget(); //工厂函数(返回右值)
Widget w; //普通对象(左值)
…
w.doWork(); //调用被左值引用限定修饰的Widget::doWork版本
//(即Widget::doWork &)
makeWidget().doWork(); //调用被右值引用限定修饰的Widget::doWork版本
//(即Widget::doWork &&)
```
后面我还会提到引用限定符修饰成员函数,但是现在,只需要记住如果基类的虚函数有引用限定符,派生类的重写就必须具有相同的引用限定符。如果没有,那么新声明的函数还是属于派生类,但是不会重写父类的任何函数。
这么多的重写需求意味着哪怕一个小小的错误也会造成巨大的不同。代码中包含重写错误通常是有效的,但它的意图不是你想要的。因此你不能指望当你犯错时编译器能通知你。比如,下面的代码是完全合法的,咋一看,还很有道理,但是它没有任何虚函数重写——没有一个派生类函数联系到基类函数。你能识别每种情况的错误吗,换句话说,为什么派生类函数没有重写同名基类函数?
```cpp
class Base {
public:
virtual void mf1() const;
virtual void mf2(int x);
virtual void mf3() &;
void mf4() const;
};
class Derived: public Base {
public:
virtual void mf1();
virtual void mf2(unsigned int x);
virtual void mf3() &&;
void mf4() const;
};
```
需要一点帮助吗?
+ `mf1`在`Base`基类声明为`const`,但是`Derived`派生类没有这个常量限定符
+ `mf2`在`Base`基类声明为接受一个`int`参数,但是在`Derived`派生类声明为接受`unsigned int`参数
+ `mf3`在`Base`基类声明为左值引用限定,但是在`Derived`派生类声明为右值引用限定
+ `mf4`在`Base`基类没有声明为`virtual`虚函数
你可能会想,“哎呀,实际操作的时候,这些warnings都能被编译器探测到,所以我不需要担心。”你说的可能对,也可能不对。就我目前检查的两款编译器来说,这些代码编译时没有任何warnings,即使我开启了输出所有warnings。(其他编译器可能会为这些问题的部分输出warnings,但不是全部。)
由于正确声明派生类的重写函数很重要,但很容易出错,C++11提供一个方法让你可以显式地指定一个派生类函数是基类版本的重写:将它声明为`override`。还是上面那个例子,我们可以这样做:
```cpp
class Derived: public Base {
public:
virtual void mf1() override;
virtual void mf2(unsigned int x) override;
virtual void mf3() && override;
virtual void mf4() const override;
};
```
代码不能编译,当然了,因为这样写的时候,编译器会抱怨所有与重写有关的问题。这也是你想要的,以及为什么要在所有重写函数后面加上`override`。
使用`override`的代码编译时看起来就像这样(假设我们的目的是`Derived`派生类中的所有函数重写`Base`基类的相应虚函数):
```cpp
class Base {
public:
virtual void mf1() const;
virtual void mf2(int x);
virtual void mf3() &;
virtual void mf4() const;
};
class Derived: public Base {
public:
virtual void mf1() const override;
virtual void mf2(int x) override;
virtual void mf3() & override;
void mf4() const override; //可以添加virtual,但不是必要
};
```
注意在这个例子中`mf4`有别于之前,它在`Base`中的声明有`virtual`修饰,所以能正常工作。大多数和重写有关的错误都是在派生类引发的,但也可能是基类的不正确导致。
比起让编译器(译注:通过warnings)告诉你想重写的而实际没有重写,不如给你的派生类重写函数全都加上`override`。如果你考虑修改修改基类虚函数的函数签名,`override`还可以帮你评估后果。如果派生类全都用上`override`,你可以只改变基类函数签名,重编译系统,再看看你造成了多大的问题(即,多少派生类不能通过编译),然后决定是否值得如此麻烦更改函数签名。没有`override`,你只能寄希望于完善的单元测试,因为,正如我们所见,派生类虚函数本想重写基类,但是没有,编译器也没有探测并发出诊断信息。
C++既有很多关键字,C++11引入了两个上下文关键字(*contextual keywords*),`override`和`final`(向虚函数添加`final`可以防止派生类重写。`final`也能用于类,这时这个类不能用作基类)。这两个关键字的特点是它们是保留的,它们只是位于特定上下文才被视为关键字。对于`override`,它只在成员函数声明结尾处才被视为关键字。这意味着如果你以前写的代码里面已经用过**override**这个名字,那么换到C++11标准你也无需修改代码:
```cpp
class Warning { //C++98潜在的传统类代码
public:
…
void override(); //C++98和C++11都合法(且含义相同)
…
};
```
关于`override`想说的就这么多,但对于成员函数引用限定(*reference qualifiers*)还有一些内容。我之前承诺我会在后面提供更多的关于它们的资料,现在就是"后面"了。
如果我们想写一个函数只接受左值实参,我们声明一个non-`const`左值引用形参:
```cpp
void doSomething(Widget& w); //只接受左值Widget对象
```
如果我们想写一个函数只接受右值实参,我们声明一个右值引用形参:
```cpp
void doSomething(Widget&& w); //只接受右值Widget对象
```
成员函数的引用限定可以很容易的区分一个成员函数被哪个对象(即`*this`)调用。它和在成员函数声明尾部添加一个`const`很相似,暗示了调用这个成员函数的对象(即`*this`)是`const`的。
对成员函数添加引用限定不常见,但是可以见。举个例子,假设我们的`Widget`类有一个`std::vector`数据成员,我们提供一个访问函数让客户端可以直接访问它:
```cpp
class Widget {
public:
using DataType = std::vector; //“using”的信息参见Item9
…
DataType& data() { return values; }
…
private:
DataType values;
};
```
这是最具封装性的设计,只给外界保留一线光。但先把这个放一边,思考一下下面的客户端代码:
```cpp
Widget w;
…
auto vals1 = w.data(); //拷贝w.values到vals1
```
`Widget::data`函数的返回值是一个左值引用(准确的说是`std::vector&`),
因为左值引用是左值,所以`vals1`是从左值初始化的。因此`vals1`由`w.values`拷贝构造而得,就像注释说的那样。
现在假设我们有一个创建`Widget`s的工厂函数,
```cpp
Widget makeWidget();
```
我们想用`makeWidget`返回的`Widget`里的`std::vector`初始化一个变量:
```cpp
auto vals2 = makeWidget().data(); //拷贝Widget里面的值到vals2
```
再说一次,`Widgets::data`返回的是左值引用,还有,左值引用是左值。所以,我们的对象(`vals2`)得从`Widget`里的`values`拷贝构造。这一次,`Widget`是`makeWidget`返回的临时对象(即右值),所以将其中的`std::vector`进行拷贝纯属浪费。最好是移动,但是因为`data`返回左值引用,C++的规则要求编译器不得不生成一个拷贝。(这其中有一些优化空间,被称作“as if rule”,但是你依赖编译器使用这个优化规则就有点傻。)(译注:“as if rule”简单来说就是在不影响程序的“外在表现”情况下做一些改变)
我们需要的是指明当`data`被右值`Widget`对象调用的时候结果也应该是一个右值。现在就可以使用引用限定,为左值`Widget`和右值`Widget`写一个`data`的重载函数来达成这一目的:
```cpp
class Widget {
public:
using DataType = std::vector;
…
DataType& data() & //对于左值Widgets,
{ return values; } //返回左值
DataType data() && //对于右值Widgets,
{ return std::move(values); } //返回右值
…
private:
DataType values;
};
```
注意`data`重载的返回类型是不同的,左值引用重载版本返回一个左值引用(即一个左值),右值引用重载返回一个临时对象(即一个右值)。这意味着现在客户端的行为和我们的期望相符了:
```cpp
auto vals1 = w.data(); //调用左值重载版本的Widget::data,
//拷贝构造vals1
auto vals2 = makeWidget().data(); //调用右值重载版本的Widget::data,
//移动构造vals2
```
这真的很棒,但别被这结尾的暖光照耀分心以致忘记了该条款的中心。这个条款的中心是只要你在派生类声明想要重写基类虚函数的函数,就加上`override`。
**请记住:**
+ 为重写函数加上`override`
+ 成员函数引用限定让我们可以区别对待左值对象和右值对象(即`*this`)
================================================
FILE: src/3.MovingToModernCpp/item13.md
================================================
## 条款十三:优先考虑`const_iterator`而非`iterator`
**Item 13: Prefer `const_iterators` to `iterators`**
STL `const_iterator`等价于指向常量的指针(pointer-to-`const`)。它们都指向不能被修改的值。标准实践是能加上`const`就加上,这也指示我们需要一个迭代器时只要没必要修改迭代器指向的值,就应当使用`const_iterator`。
上面的说法对C++11和C++98都是正确的,但是在C++98中,标准库对`const_iterator`的支持不是很完整。首先不容易创建它们,其次就算你有了它,它的使用也是受限的。假如你想在`std::vector`中查找第一次出现1983(C++代替C with classes的那一年)的位置,然后插入1998(第一个ISO C++标准被接纳的那一年)。如果*vector*中没有1983,那么就在*vector*尾部插入。在C++98中使用`iterator`可以很容易做到:
```cpp
std::vector values;
…
std::vector::iterator it =
std::find(values.begin(), values.end(), 1983);
values.insert(it, 1998);
```
但是这里`iterator`真的不是一个好的选择,因为这段代码不修改`iterator`指向的内容。用`const_iterator`重写这段代码是很平常的,但是在C++98中就不是了。下面是一种概念上可行但是不正确的方法:
```cpp
typedef std::vector::iterator IterT; //typedef
typedef std::vector::const_iterator ConstIterT;
std::vector values;
…
ConstIterT ci =
std::find(static_cast(values.begin()), //cast
static_cast(values.end()), //cast
1983);
values.insert(static_cast(ci), 1998); //可能无法通过编译,
//原因见下
```
`typedef`不是强制的,但是可以让代码中的*cast*更好写。(你可能想知道为什么我使用`typedef`而不是[Item9](../3.MovingToModernCpp/item9.md)提到的别名声明,因为这段代码在演示C++98做法,别名声明是C++11加入的特性)
之所以`std::find`的调用会出现类型转换是因为在C++98中`values`是non-`const`容器,没办法简简单单的从non-`const`容器中获取`const_iterator`。严格来说类型转换不是必须的,因为用其他方法获取`const_iterator`也是可以的(比如你可以把`values`绑定到reference-to-`const`变量上,然后再用这个变量代替`values`),但不管怎么说,从non-`const`容器中获取`const_iterator`的做法都有点别扭。
当你费劲地获得了`const_iterator`,事情可能会变得更糟,因为C++98中,插入操作(以及删除操作)的位置只能由`iterator`指定,`const_iterator`是不被接受的。这也是我在上面的代码中,将`const_iterator`(我那么小心地从`std::find`搞出来的东西)转换为`iterator`的原因,因为向`insert`传入`const_iterator`不能通过编译。
老实说,上面的代码也可能无法编译,因为没有一个可移植的从`const_iterator`到`iterator`的方法,即使使用`static_cast`也不行。甚至传说中的牛刀`reinterpret_cast`也杀不了这条鸡。(它不是C++98的限制,也不是C++11的限制,只是`const_iterator`就是不能转换为`iterator`,不管看起来对它们施以转换是有多么合理。)不过有办法生成一个`iterator`,使其指向和`const_iterator`指向相同,但是看起来不明显,也没有广泛应用,在这本书也不值得讨论。除此之外,我希望目前我陈述的观点是清晰的:`const_iterator`在C++98中会有很多问题,不如它的兄弟(译注:指`iterator`)有用。最终,开发者们不再相信能加`const`就加它的教条,而是只在实用的地方加它,C++98的`const_iterator`不是那么实用。
所有的这些都在C++11中改变了,现在`const_iterator`既容易获取又容易使用。容器的成员函数`cbegin`和`cend`产出`const_iterator`,甚至对于non-`const`容器也可用,那些之前使用*iterator*指示位置(如`insert`和`erase`)的STL成员函数也可以使用`const_iterator`了。使用C++11 `const_iterator`重写C++98使用`iterator`的代码也稀松平常:
```cpp
std::vector values; //和之前一样
…
auto it = //使用cbegin
std::find(values.cbegin(), values.cend(), 1983);//和cend
values.insert(it, 1998);
```
现在使用`const_iterator`的代码就很实用了!
唯一一个C++11对于`const_iterator`支持不足(译注:C++14支持但是C++11的时候还没)的情况是:当你想写最大程度通用的库,并且这些库代码为一些容器和类似容器的数据结构提供`begin`、`end`(以及`cbegin`,`cend`,`rbegin`,`rend`等)作为**非成员函数**而不是成员函数时。其中一种情况就是原生数组,还有一种情况是一些只由自由函数组成接口的第三方库。(译注:自由函数*free function*,指的是非成员函数,即一个函数,只要不是成员函数就可被称作*free function*)最大程度通用的库会考虑使用非成员函数而不是假设成员函数版本存在。
举个例子,我们可以泛化下面的`findAndInsert`:
```cpp
template
void findAndInsert(C& container, //在容器中查找第一次
const V& targetVal, //出现targetVal的位置,
const V& insertVal) //然后在那插入insertVal
{
using std::cbegin;
using std::cend;
auto it = std::find(cbegin(container), //非成员函数cbegin
cend(container), //非成员函数cend
targetVal);
container.insert(it, insertVal);
}
```
它可以在C++14工作良好,但是很遗憾,C++11不在良好之列。由于标准化的疏漏,C++11只添加了非成员函数`begin`和`end`,但是没有添加`cbegin`,`cend`,`rbegin`,`rend`,`crbegin`,`crend`。C++14修订了这个疏漏。
如果你使用C++11,并且想写一个最大程度通用的代码,而你使用的STL没有提供缺失的非成员函数`cbegin`和它的朋友们,你可以简单的写下你自己的实现。比如,下面就是非成员函数`cbegin`的实现:
```cpp
template
auto cbegin(const C& container)->decltype(std::begin(container))
{
return std::begin(container); //解释见下
}
```
你可能很惊讶非成员函数`cbegin`没有调用成员函数`cbegin`吧?我也是。但是请跟逻辑走。这个`cbegin`模板接受任何代表类似容器的数据结构的实参类型`C`,并且通过reference-to-`const`形参`container`访问这个实参。如果`C`是一个普通的容器类型(如`std::vector`),`container`将会引用一个`const`版本的容器(如`const std::vector&`)。对`const`容器调用非成员函数`begin`(由C++11提供)将产出`const_iterator`,这个迭代器也是模板要返回的。用这种方法实现的好处是就算容器只提供`begin`成员函数(对于容器来说,C++11的非成员函数`begin`调用这些成员函数)不提供`cbegin`成员函数也没问题。那么现在你可以将这个非成员函数`cbegin`施于只直接支持`begin`的容器。
如果`C`是原生数组,这个模板也能工作。这时,`container`成为一个`const`数组的引用。C++11为数组提供特化版本的非成员函数`begin`,它返回指向数组第一个元素的指针。一个`const`数组的元素也是`const`,所以对于`const`数组,非成员函数`begin`返回指向`const`的指针(pointer-to-`const`)。在数组的上下文中,所谓指向`const`的指针(pointer-to-`const`),也就是`const_iterator`了。
回到最开始,本条款的中心是鼓励你只要能就使用`const_iterator`。最原始的动机——只要它有意义就加上`const`——是C++98就有的思想。但是在C++98,它(译注:`const_iterator`)只是一般有用,到了C++11,它就是极其有用了,C++14在其基础上做了些修补工作。
**请记住:**
+ 优先考虑`const_iterator`而非`iterator`
+ 在最大程度通用的代码中,优先考虑非成员函数版本的`begin`,`end`,`rbegin`等,而非同名成员函数
================================================
FILE: src/3.MovingToModernCpp/item14.md
================================================
## 条款十四:如果函数不抛出异常请使用`noexcept`
**Item 14: Declare functions `noexcept` if they won’t emit exceptions**
在C++98中,异常说明(*exception specifications*)是喜怒无常的野兽。你不得不写出函数可能抛出的异常类型,如果函数实现有所改变,异常说明也可能需要修改。改变异常说明会影响客户端代码,因为调用者可能依赖原版本的异常说明。编译器不会在函数实现,异常说明和客户端代码之间提供一致性保障。大多数程序员最终都认为不值得为C++98的异常说明做得如此麻烦。
在C++11标准化过程中,大家一致认为异常说明真正有用的信息是一个函数是否会抛出异常。非黑即白,一个函数可能抛异常,或者不会。这种"可能-绝不"的二元论构成了C++11异常说的基础,从根本上改变了C++98的异常说明。(C++98风格的异常说明也有效,但是已经标记为deprecated(废弃))。在C++11中,无条件的`noexcept`保证函数不会抛出任何异常。
关于一个函数是否已经声明为`noexcept`是接口设计的事。函数的异常抛出行为是客户端代码最关心的。调用者可以查看函数是否声明为`noexcept`,这个可以影响到调用代码的异常安全性(*exception safety*)和效率。就其本身而言,函数是否为`noexcept`和成员函数是否`const`一样重要。当你知道这个函数不会抛异常而没加上`noexcept`,那这个接口说明就有点差劲了。
不过这里还有给不抛异常的函数加上`noexcept`的动机:它允许编译器生成更好的目标代码。要想知道为什么,了解C++98和C++11指明一个函数不抛异常的方式是很有用了。考虑一个函数`f`,它保证调用者永远不会收到一个异常。两种表达方式如下:
```cpp
int f(int x) throw(); //C++98风格,没有来自f的异常
int f(int x) noexcept; //C++11风格,没有来自f的异常
```
如果在运行时,`f`出现一个异常,那么就和`f`的异常说明冲突了。在C++98的异常说明中,调用栈(the *call stack*)会展开至`f`的调用者,在一些与这地方不相关的动作后,程序被终止。C++11异常说明的运行时行为有些不同:调用栈只是**可能**在程序终止前展开。
展开调用栈和**可能**展开调用栈两者对于代码生成(code generation)有非常大的影响。在一个`noexcept`函数中,当异常可能传播到函数外时,优化器不需要保证运行时栈(the runtime stack)处于可展开状态;也不需要保证当异常离开`noexcept`函数时,`noexcept`函数中的对象按照构造的反序析构。而标注“`throw()`”异常声明的函数缺少这样的优化灵活性,没加异常声明的函数也一样。可以总结一下:
```cpp
RetType function(params) noexcept; //极尽所能优化
RetType function(params) throw(); //较少优化
RetType function(params); //较少优化
```
这是一个充分的理由使得你当知道它不抛异常时加上`noexcept`。
还有一些函数更符合这个情况。移动操作是绝佳的例子。假如你有一份C++98代码,里面用到了`std::vector`。`Widget`通过`push_back`一次又一次的添加进`std::vector`:
```cpp
std::vector vw;
…
Widget w;
… //用w做点事
vw.push_back(w); //把w添加进vw
…
```
假设这个代码能正常工作,你也无意修改为C++11风格。但是你确实想要C++11移动语义带来的性能优势,毕竟这里的类型是可以移动的(move-enabled types)。因此你需要确保`Widget`有移动操作,可以手写代码也可以让编译器自动生成,当然前提是能满足自动生成的条件(参见[Item17](../3.MovingToModernCpp/item17.md))。
当新元素添加到`std::vector`,`std::vector`可能没地方放它,换句话说,`std::vector`的大小(size)等于它的容量(capacity)。这时候,`std::vector`会分配一个新的更大块的内存用于存放其中元素,然后将元素从老内存区移动到新内存区,然后析构老内存区里的对象。在C++98中,移动是通过复制老内存区的每一个元素到新内存区完成的,然后老内存区的每个元素发生析构。这种方法使得`push_back`可以提供很强的异常安全保证:如果在复制元素期间抛出异常,`std::vector`状态保持不变,因为老内存元素析构必须建立在它们已经成功复制到新内存的前提下。
在C++11中,一个很自然的优化就是将上述复制操作替换为移动操作。但是很不幸运,这会破坏`push_back`的异常安全保证。如果**n**个元素已经从老内存移动到了新内存区,但异常在移动第**n+1**个元素时抛出,那么`push_back`操作就不能完成。但是原始的`std::vector`已经被修改:有**n**个元素已经移动走了。恢复`std::vector`至原始状态也不太可能,因为从新内存移动到老内存本身又可能引发异常。
这是个很严重的问题,因为老代码可能依赖于`push_back`提供的强烈的异常安全保证。因此,C++11版本的实现不能简单的将`push_back`里面的复制操作替换为移动操作,除非知晓移动操作绝不抛异常,这时复制替换为移动就是安全的,唯一的副作用就是性能得到提升。
`std::vector::push_back`受益于“如果可以就移动,如果必要则复制”策略,并且它不是标准库中唯一采取该策略的函数。C++98中还有一些函数(如`std::vector::reserve`,`std::deque::insert`等)也受益于这种强异常保证。对于这个函数只有在知晓移动不抛异常的情况下用C++11的移动操作替换C++98的复制操作才是安全的。但是如何知道一个函数中的移动操作是否产生异常?答案很明显:它检查这个操作是否被声明为`noexcept`。(这个检查非常弯弯绕。像是`std::vector::push_back`之类的函数调用`std::move_if_noexcept`,这是个`std::move`的变体,根据其中类型的移动构造函数是否为`noexcept`的,视情况转换为右值或保持左值(参见[Item23](../5.RRefMovSemPerfForw/item23.md))。反过来,`std::move_if_noexcept`查阅`std::is_nothrow_move_constructible`这个*type trait*,基于移动构造函数是否有`noexcept`(或者`throw()`)的设计,编译器设置这个*type trait*的值。)
`swap`函数是`noexcept`的另一个绝佳用地。`swap`是STL算法实现的一个关键组件,它也常用于拷贝运算符重载中。它的广泛使用意味着对其施加不抛异常的优化是非常有价值的。有趣的是,标准库的`swap`是否`noexcept`有时依赖于用户定义的`swap`是否`noexcept`。比如,数组和`std::pair`的`swap`声明如下:
```cpp
template
void swap(T (&a)[N],
T (&b)[N]) noexcept(noexcept(swap(*a, *b))); //见下文
template
struct pair {
…
void swap(pair& p) noexcept(noexcept(swap(first, p.first)) &&
noexcept(swap(second, p.second)));
…
};
```
这些函数**视情况**`noexcept`:它们是否`noexcept`依赖于`noexcept`声明中的表达式是否`noexcept`。假设有两个`Widget`数组,交换数组操作为`noexcept`的前提是数组中的元素交换是`noexcept`的,即`Widget`的`swap`是`noexcept`。因此`Widget`的`swap`的作者决定了交换`widget`的数组是否`noexcept`。对于`Widget`的交换是否`noexcept`决定了对于`Widget`数组的交换是否`noexcept`,以及其他交换,比如`Widget`的数组的数组的交换是否`noexcept`。类似地,交换两个含有`Widget`的`std::pair`是否`noexcept`依赖于`Widget`的`swap`是否`noexcept`。事实上交换高层次数据结构是否`noexcept`取决于它的构成部分的那些低层次数据结构是否`noexcept`,这激励你只要可以就提供`noexcept` `swap`函数(译注:因为如果你的函数不提供`noexcept`保证,其它依赖你的高层次`swap`就不能保证`noexcept`)。
现在,我希望你能为`noexcept`提供的优化机会感到高兴,同时我还得让你缓一缓别太高兴了。优化很重要,但是正确性更重要。我在这个条款的开头提到`noexcept`是函数接口的一部分,所以仅当你保证一个函数实现在长时间内不会抛出异常时才声明`noexcept`。如果你声明一个函数为`noexcept`,但随即又后悔了,你没有选择。你可以从函数声明中移除`noexcept`(即改变它的接口),这理所当然会影响客户端代码。你可以改变实现使得这个异常可以避免,再保留原版本(现在来看不正确的)异常说明。如果你这么做,在异常试图离开这个函数时程序将会终止。或者你就顺从了既有实现,舍弃了激起你兴趣的东西,从一开始就改变实现。这些选择都不尽人意。
这个问题的本质是实际上大多数函数都是异常中立(*exception-neutral*)的。这些函数自己不抛异常,但是它们内部的调用可能抛出。此时,异常中立函数允许那些抛出异常的函数在调用链上更进一步直到遇到异常处理程序,而不是就地终止。异常中立函数决不应该声明为`noexcept`,因为它们可能抛出那种“让它们过吧”的异常(译注:也就是说在当前这个函数内不处理异常,但是又不立即终止程序,而是让调用这个函数的函数处理异常。)因此大多数函数缺少`noexcept`设计。
然而,一些函数很自然的不应该抛异常,更进一步——尤其是移动操作和`swap`——使其`noexcept`有重大意义,只要可能就应该将它们实现为`noexcept`。(STL对容器的移动操作的接口规范里缺少`noexcept`。然而实现者可以增强STL函数的异常说明,实际上,至少有些容器的移动操作已被声明为`noexcept`,这些做法就是本条例所给建议的好示例。发现了容器移动操作可以写成不抛异常的之后,实现者经常将这些操作声明为`noexcept`的,尽管标准并没有要求他们这么做。)老实说,当你确保函数决不抛异常的时候,一定要将它们声明为`noexcept`。
请注意我说有些函数有**自然的**`noexcept`实现法。为了`noexcept`而扭曲函数实现来达成目的是本末倒置。是把马车放到马前,是一叶障目不见泰山。是...选择你喜欢的比喻吧。(译注:几个英语熟语,都是想说明“本末倒置”。)如果一个简单的函数实现可能引发异常(比如调用一个可能抛异常的函数),而你为了讨好调用者隐藏了这个(比如捕获所有异常,然后替换为状态码或者特殊返回值),这不仅会使你的函数实现变得复杂,还会让调用点的代码变得复杂。调用者可能不得不检查状态码或特殊返回值。而这些复杂的运行时开销(比如额外的分支,大的函数给指令缓存带来的压力等)可能超出`noexcept`带来的性能提升,再加上你会悲哀的发现这些代码又难读又难维护。那是糟糕的软件工程化。
对于一些函数,使其成为`noexcept`是很重要的,它们应当默认如是。在C++98,允许内存释放(memory deallocation)函数(即`operator delete`和`operator delete[]`)和析构函数抛出异常是糟糕的代码设计,C++11将这种作风升级为语言规则。默认情况下,内存释放函数和析构函数——不管是用户定义的还是编译器生成的——都是隐式`noexcept`。因此它们不需要声明`noexcept`。(这么做也不会有问题,只是不合常规)。析构函数非隐式`noexcept`的情况仅当类的数据成员(包括继承的成员还有继承成员内的数据成员)明确声明它的析构函数可能抛出异常(如声明“`noexcept(false)`”)。这种析构函数不常见,标准库里面没有。如果一个对象的析构函数可能被标准库使用(比如在容器内或者被传给一个算法),析构函数又可能抛异常,那么程序的行为是未定义的。
值得注意的是一些库接口设计者会区分有宽泛契约(**wild contracts**)和严格契约(**narrow contracts**)的函数。有宽泛契约的函数没有前置条件。这种函数不管程序状态如何都能调用,它对调用者传来的实参不设约束。(“不管程序状态如何”和“不设约束”对已经行为未定义的程序无效。比如`std::vector::size`有宽泛契约,但是并不保证如果你把一块随机内存转换为一个`std::vector`,在这块内存上调用它会有合理的表现。转换的结果是未定义的,所以包含这个转换的程序也无法保证表现合理)宽泛契约的函数决不表现出未定义行为。
反之,没有宽泛契约的函数就有严格契约。对于这些函数,如果违反前置条件,结果将会是未定义的。
如果你写了一个有宽泛契约的函数并且你知道它不会抛异常,那么遵循这个条款给它声明一个`noexcept`是很容易的。对于严格契约的函数,情况就有点微妙了。举个例子,假如你在写一个形参为`std::string`的函数`f`,并且假定这个函数`f`很自然的决不引发异常。这就表明`f`应该被声明为`noexcept`。
现在假如`f`有一个前置条件:类型为`std::string`的参数的长度不能超过32个字符。如果现在调用`f`并传给它一个大于32字符的`std::string`,函数行为将是未定义的,因为**根据定义**违反了前置条件,导致了未定义行为。`f`没有义务去检查前置条件,它假设这些前置条件都是满足的。(调用者有责任确保参数字符不超过32字符等这些假设有效。)即使有前置条件,将`f`声明为`noexcept`似乎也是合适的:
```cpp
void f(const std::string& s) noexcept; //前置条件:
//s.length() <= 32
```
假定`f`的实现者在函数里面检查前置条件冲突。虽然检查是没有必要的,但是也没禁止这么做,检查前置条件可能也是有用的,比如在系统测试时。debug一个抛出的异常一般都比跟踪未定义行为起因更容易。那么怎么报告前置条件冲突使得测试工具或客户端错误处理程序能检测到它呢?简单直接的做法是抛出“precondition was violated”异常,但是如果`f`声明了`noexcept`,这就行不通了;抛出一个异常会导致程序终止。因为这个原因,区分严格/宽泛契约库设计者一般会将`noexcept`留给宽泛契约函数。
作为结束语,让我详细说明一下之前的观察,即编译器不会为函数实现和异常规范提供一致性保障。考虑下面的代码,它是完全正确的:
```cpp
void setup(); //函数定义另在一处
void cleanup();
void doWork() noexcept
{
setup(); //设置要做的工作
… //真实工作
cleanup(); //执行清理动作
}
```
这里,`doWork`声明为`noexcept`,即使它调用了non-`noexcept`函数`setup`和`cleanup`。看起来有点矛盾,其实可以猜想`setup`和`cleanup`在文档上写明了它们决不抛出异常,即使它们没有写上`noexcept`。至于为什么明明不抛异常却不写`noexcept`也是有合理原因的。比如,它们可能是用C写的库函数的一部分。(即使一些函数从C标准库移动到了`std`命名空间,也可能缺少异常规范,`std::strlen`就是一个例子,它没有声明`noexcept`。)或者它们可能是C++98库的一部分,它们不使用C++98异常规范,到了C++11还没有修订。
因为有很多合理原因解释为什么`noexcept`依赖于缺少`noexcept`保证的函数,所以C++允许这些代码,编译器一般也不会给出warnings。
**请记住:**
+ `noexcept`是函数接口的一部分,这意味着调用者可能会依赖它
+ `noexcept`函数较之于non-`noexcept`函数更容易优化
+ `noexcept`对于移动语义,`swap`,内存释放函数和析构函数非常有用
+ 大多数函数是异常中立的(译注:可能抛也可能不抛异常)而不是`noexcept`
================================================
FILE: src/3.MovingToModernCpp/item15.md
================================================
## 条款十五:尽可能的使用`constexpr`
**Item 15: Use `constexpr` whenever possible**
如果要给C++11颁一个“最令人困惑新词”奖,`constexpr`十有八九会折桂。当用于对象上面,它本质上就是`const`的加强形式,但是当它用于函数上,意思就大不相同了。有必要消除困惑,因为你绝对会用它的,特别是当你发现`constexpr` “正合吾意”的时候。
从概念上来说,`constexpr`表明一个值不仅仅是常量,还是编译期可知的。这个表述并不全面,因为当`constexpr`被用于函数的时候,事情就有一些细微差别了。为了避免我毁了结局带来的surprise,我现在只想说,你不能假设`constexpr`函数的结果是`const`,也不能保证它们的(译注:返回)值是在编译期可知的。最有意思的是,这些是**特性**。关于`constexpr`函数返回的结果不需要是`const`,也不需要编译期可知这一点是**良好的**行为!
不过我们还是先从`constexpr`对象开始说起。这些对象,实际上,和`const`一样,它们是编译期可知的。(技术上来讲,它们的值在翻译期(translation)决议,所谓翻译不仅仅包含是编译(compilation)也包含链接(linking),除非你准备写C++的编译器和链接器,否则这些对你不会造成影响,所以你编程时无需担心,把这些`constexpr`对象值看做编译期决议也无妨的。)
编译期可知的值“享有特权”,它们可能被存放到只读存储空间中。对于那些嵌入式系统的开发者,这个特性是相当重要的。更广泛的应用是“其值编译期可知”的常量整数会出现在需要“整型常量表达式(**integral constant expression**)的上下文中,这类上下文包括数组大小,整数模板参数(包括`std::array`对象的长度),枚举名的值,对齐修饰符(译注:[`alignas(val)`](https://en.cppreference.com/w/cpp/language/alignas)),等等。如果你想在这些上下文中使用变量,你一定会希望将它们声明为`constexpr`,因为编译器会确保它们是编译期可知的:
```cpp
int sz; //non-constexpr变量
…
constexpr auto arraySize1 = sz; //错误!sz的值在
//编译期不可知
std::array data1; //错误!一样的问题
constexpr auto arraySize2 = 10; //没问题,10是
//编译期可知常量
std::array data2; //没问题, arraySize2是constexpr
```
注意`const`不提供`constexpr`所能保证之事,因为`const`对象不需要在编译期初始化它的值。
```cpp
int sz; //和之前一样
…
const auto arraySize = sz; //没问题,arraySize是sz的const复制
std::array data; //错误,arraySize值在编译期不可知
```
简而言之,所有`constexpr`对象都是`const`,但不是所有`const`对象都是`constexpr`。如果你想编译器保证一个变量有一个值,这个值可以放到那些需要编译期常量(compile-time constants)的上下文的地方,你需要的工具是`constexpr`而不是`const`。
涉及到`constexpr`函数时,`constexpr`对象的使用情况就更有趣了。如果实参是编译期常量,这些函数将产出编译期常量;如果实参是运行时才能知道的值,它们就将产出运行时值。这听起来就像你不知道它们要做什么一样,那么想是错误的,请这么看:
+ `constexpr`函数可以用于需求编译期常量的上下文。在需求编译期常量的上下文中,如果你传给`constexpr`函数的实参在编译期可知,那么结果将在编译期计算;如果实参的值在编译期不知道,你的代码就会被拒绝。
+ 当一个`constexpr`函数被一个或者多个编译期不可知值调用时,它就像普通函数一样,运行时计算它的结果。这意味着你不需要两个函数,一个用于编译期计算,一个用于运行时计算。`constexpr`全做了。
假设我们需要一个数据结构来存储一个实验的结果,而这个实验可能以各种方式进行。实验期间风扇转速,温度等等都可能导致亮度值改变,亮度值可以是高,低,或者无。如果有**n**个实验相关的环境条件,它们每一个都有三个状态,最终可以得到的组合有3n个。储存所有实验结果的所有组合需要足够存放3n个值的数据结构。假设每个结果都是`int`并且**n**是编译期已知的(或者可以被计算出的),一个`std::array`是一个合理的选择。我们需要一个方法在编译期计算3n。C++标准库提供了`std::pow`,它的数学功能正是我们所需要的,但是,对我们来说,这里还有两个问题。第一,`std::pow`是为浮点类型设计的,我们需要整型结果。第二,`std::pow`不是`constexpr`(即,不保证使用编译期可知值调用而得到编译期可知的结果),所以我们不能用它作为`std::array`的大小。
幸运的是,我们可以应需写个`pow`。我将展示怎么快速完成它,不过现在让我们先看看它应该怎么被声明和使用:
```cpp
constexpr //pow是绝不抛异常的
int pow(int base, int exp) noexcept //constexpr函数
{
… //实现在下面
}
constexpr auto numConds = 5; //(上面例子中)条件的个数
std::array results; //结果有3^numConds个元素
```
回忆下`pow`前面的`constexpr`不表明`pow`返回一个`const`值,它只说了如果`base`和`exp`是编译期常量,`pow`的值可以被当成编译期常量使用。如果`base`和/或`exp`不是编译期常量,`pow`结果将会在运行时计算。这意味着`pow`不止可以用于像`std::array`的大小这种需要编译期常量的地方,它也可以用于运行时环境:
```cpp
auto base = readFromDB("base"); //运行时获取这些值
auto exp = readFromDB("exponent");
auto baseToExp = pow(base, exp); //运行时调用pow函数
```
因为`constexpr`函数必须能在编译期值调用的时候返回编译期结果,就必须对它的实现施加一些限制。这些限制在C++11和C++14标准间有所出入。
C++11中,`constexpr`函数的代码不超过一行语句:一个`return`。听起来很受限,但实际上有两个技巧可以扩展`constexpr`函数的表达能力。第一,使用三元运算符“`?:`”来代替`if`-`else`语句,第二,使用递归代替循环。因此`pow`可以像这样实现:
```cpp
constexpr int pow(int base, int exp) noexcept
{
return (exp == 0 ? 1 : base * pow(base, exp - 1));
}
```
这样没问题,但是很难想象除了使用函数式语言的程序员外会觉得这样硬核的编程方式更好。在C++14中,`constexpr`函数的限制变得非常宽松了,所以下面的函数实现成为了可能:
```cpp
constexpr int pow(int base, int exp) noexcept //C++14
{
auto result = 1;
for (int i = 0; i < exp; ++i) result *= base;
return result;
}
```
`constexpr`函数限制为只能获取和返回**字面值类型**,这基本上意味着那些有了值的类型能在编译期决定。在C++11中,除了`void`外的所有内置类型,以及一些用户定义类型都可以是字面值类型,因为构造函数和其他成员函数可能是`constexpr`:
```cpp
class Point {
public:
constexpr Point(double xVal = 0, double yVal = 0) noexcept
: x(xVal), y(yVal)
{}
constexpr double xValue() const noexcept { return x; }
constexpr double yValue() const noexcept { return y; }
void setX(double newX) noexcept { x = newX; }
void setY(double newY) noexcept { y = newY; }
private:
double x, y;
};
```
`Point`的构造函数可被声明为`constexpr`,因为如果传入的参数在编译期可知,`Point`的数据成员也能在编译器可知。因此这样初始化的`Point`就能为`constexpr`:
```cpp
constexpr Point p1(9.4, 27.7); //没问题,constexpr构造函数
//会在编译期“运行”
constexpr Point p2(28.8, 5.3); //也没问题
```
类似的,`xValue`和`yValue`的*getter*(取值器)函数也能是`constexpr`,因为如果对一个编译期已知的`Point`对象(如一个`constexpr` `Point`对象)调用*getter*,数据成员`x`和`y`的值也能在编译期知道。这使得我们可以写一个`constexpr`函数,里面调用`Point`的*getter*并初始化`constexpr`的对象:
```cpp
constexpr
Point midpoint(const Point& p1, const Point& p2) noexcept
{
return { (p1.xValue() + p2.xValue()) / 2, //调用constexpr
(p1.yValue() + p2.yValue()) / 2 }; //成员函数
}
constexpr auto mid = midpoint(p1, p2); //使用constexpr函数的结果
//初始化constexpr对象
```
这太令人激动了。它意味着`mid`对象通过调用构造函数,*getter*和非成员函数来进行初始化过程就能在只读内存中被创建出来!它也意味着你可以在模板实参或者需要枚举名的值的表达式里面使用像`mid.xValue() * 10`的表达式!(因为`Point::xValue`返回`double`,`mid.xValue() * 10`也是个`double`。浮点数类型不可被用于实例化模板或者说明枚举名的值,但是它们可以被用来作为产生整数值的大表达式的一部分。比如,`static_cast(mid.xValue() * 10)`可以被用来实例化模板或者说明枚举名的值。)它也意味着以前相对严格的编译期完成的工作和运行时完成的工作的界限变得模糊,一些传统上在运行时的计算过程能并入编译时。越多这样的代码并入,你的程序就越快。(然而,编译会花费更长时间)
在C++11中,有两个限制使得`Point`的成员函数`setX`和`setY`不能声明为`constexpr`。第一,它们修改它们操作的对象的状态, 并且在C++11中,`constexpr`成员函数是隐式的`const`。第二,它们有`void`返回类型,`void`类型不是C++11中的字面值类型。这两个限制在C++14中放开了,所以C++14中`Point`的*setter*(赋值器)也能声明为`constexpr`:
```cpp
class Point {
public:
…
constexpr void setX(double newX) noexcept { x = newX; } //C++14
constexpr void setY(double newY) noexcept { y = newY; } //C++14
…
};
```
现在也能写这样的函数:
```cpp
//返回p相对于原点的镜像
constexpr Point reflection(const Point& p) noexcept
{
Point result; //创建non-const Point
result.setX(-p.xValue()); //设定它的x和y值
result.setY(-p.yValue());
return result; //返回它的副本
}
```
客户端代码可以这样写:
```cpp
constexpr Point p1(9.4, 27.7); //和之前一样
constexpr Point p2(28.8, 5.3);
constexpr auto mid = midpoint(p1, p2);
constexpr auto reflectedMid = //reflectedMid的值
reflection(mid); //(-19.1, -16.5)在编译期可知
```
本条款的建议是尽可能的使用`constexpr`,现在我希望大家已经明白缘由:`constexpr`对象和`constexpr`函数可以使用的范围比non-`constexpr`对象和函数大得多。使用`constexpr`关键字可以最大化你的对象和函数可以使用的场景。
还有个重要的需要注意的是`constexpr`是对象和函数接口的一部分。加上`constexpr`相当于宣称“我能被用在C++要求常量表达式的地方”。如果你声明一个对象或者函数是`constexpr`,客户端程序员就可能会在那些场景中使用它。如果你后面认为使用`constexpr`是一个错误并想移除它,你可能造成大量客户端代码不能编译。(为了debug或者性能优化而添加I/O到一个函数中这样简单的动作可能就导致这样的问题,因为I/O语句一般不被允许出现在`constexpr`函数里)“尽可能”的使用`constexpr`表示你需要长期坚持对某个对象或者函数施加这种限制。
**请记住:**
+ `constexpr`对象是`const`,它被在编译期可知的值初始化
+ 当传递编译期可知的值时,`constexpr`函数可以产出编译期可知的结果
+ `constexpr`对象和函数可以使用的范围比non-`constexpr`对象和函数要大
+ `constexpr`是对象和函数接口的一部分
================================================
FILE: src/3.MovingToModernCpp/item16.md
================================================
## 条款十六:让`const`成员函数线程安全
**Item 16: Make `const` member functions thread safe**
如果我们在数学领域中工作,我们就会发现用一个类表示多项式是很方便的。在这个类中,使用一个函数来计算多项式的根是很有用的,也就是多项式的值为零的时候(译者注:通常也被叫做零点,即使得多项式值为零的那些取值)。这样的一个函数它不会更改多项式。所以,它自然被声明为`const`函数。
```c++
class Polynomial {
public:
using RootsType = //数据结构保存多项式为零的值
std::vector; //(“using” 的信息查看条款9)
…
RootsType roots() const;
…
};
```
计算多项式的根是很复杂的,因此如果不需要的话,我们就不做。如果必须做,我们肯定不想再做第二次。所以,如果必须计算它们,就缓存多项式的根,然后实现`roots`来返回缓存的值。下面是最基本的实现:
```c++
class Polynomial {
public:
using RootsType = std::vector;
RootsType roots() const
{
if (!rootsAreValid) { //如果缓存不可用
… //计算根
//用rootVals存储它们
rootsAreValid = true;
}
return rootVals;
}
private:
mutable bool rootsAreValid{ false }; //初始化器(initializer)的
mutable RootsType rootVals{}; //更多信息请查看条款7
};
```
从概念上讲,`roots`并不改变它所操作的`Polynomial`对象。但是作为缓存的一部分,它也许会改变`rootVals`和`rootsAreValid`的值。这就是`mutable`的经典使用样例,这也是为什么它是数据成员声明的一部分。
假设现在有两个线程同时调用`Polynomial`对象的`roots`方法:
```c++
Polynomial p;
…
/*------ Thread 1 ------*/ /*-------- Thread 2 --------*/
auto rootsOfp = p.roots(); auto valsGivingZero = p.roots();
```
这些用户代码是非常合理的。`roots`是`const`成员函数,那就表示着它是一个读操作。在没有同步的情况下,让多个线程执行读操作是安全的。它最起码应该做到这点。在本例中却没有做到线程安全。因为在`roots`中,这些线程中的一个或两个可能尝试修改成员变量`rootsAreValid`和`rootVals`。这就意味着在没有同步的情况下,这些代码会有不同的线程读写相同的内存,这就是数据竞争(*data race*)的定义。这段代码的行为是未定义的。
问题就是`roots`被声明为`const`,但不是线程安全的。`const`声明在C++11中与在C++98中一样正确(检索多项式的根并不会更改多项式的值),因此需要纠正的是线程安全的缺乏。
解决这个问题最普遍简单的方法就是——使用`mutex`(互斥量):
```c++
class Polynomial {
public:
using RootsType = std::vector;
RootsType roots() const
{
std::lock_guard g(m); //锁定互斥量
if (!rootsAreValid) { //如果缓存无效
… //计算/存储根值
rootsAreValid = true;
}
return rootsVals;
} //解锁互斥量
private:
mutable std::mutex m;
mutable bool rootsAreValid { false };
mutable RootsType rootsVals {};
};
```
`std::mutex m`被声明为`mutable`,因为锁定和解锁它的都是non-`const`成员函数。在`roots`(`const`成员函数)中,`m`却被视为`const`对象。
~~值得注意的是,因为`std::mutex`是一种只可移动类型(*move-only type*,一种可以移动但不能复制的类型),所以将`m`添加进`Polynomial`中的副作用是使`Polynomial`失去了被复制的能力。不过,它仍然可以移动。~~ (译者注:实际上 `std::mutex` 既不可移动,也不可复制。因而包含他们的类也同时是不可移动和不可复制的。)
在某些情况下,互斥量的副作用会显得过大。例如,如果你所做的只是计算成员函数被调用了多少次,使用`std::atomic` 修饰的计数器(保证其他线程视它的操作为不可分割的整体,参见[item40](../7.TheConcurrencyAPI/item40.md))通常会是一个开销更小的方法。(然而它是否轻量取决于你使用的硬件和标准库中互斥量的实现。)以下是如何使用`std::atomic`来统计调用次数。
```c++
class Point { //2D点
public:
…
double distanceFromOrigin() const noexcept //noexcept的使用
{ //参考条款14
++callCount; //atomic的递增
return std::sqrt((x * x) + (y * y));
}
private:
mutable std::atomic callCount{ 0 };
double x, y;
};
```
~~与`std::mutex`一样,`std::atomic`是只可移动类型,所以在`Point`中存在`callCount`就意味着`Point`也是只可移动的。~~(译者注:与 `std::mutex` 类似的,实际上 `std::atomic` 既不可移动,也不可复制。因而包含他们的类也同时是不可移动和不可复制的。)
因为对`std::atomic`变量的操作通常比互斥量的获取和释放的消耗更小,所以你可能会过度倾向与依赖`std::atomic`。例如,在一个类中,缓存一个开销昂贵的`int`,你就会尝试使用一对`std::atomic`变量而不是互斥量。
```c++
class Widget {
public:
…
int magicValue() const
{
if (cacheValid) return cachedValue;
else {
auto val1 = expensiveComputation1();
auto val2 = expensiveComputation2();
cachedValue = val1 + val2; //第一步
cacheValid = true; //第二步
return cachedValid;
}
}
private:
mutable std::atomic cacheValid{ false };
mutable std::atomic cachedValue;
};
```
这是可行的,但难以避免有时出现重复计算的情况。考虑:
+ 一个线程调用`Widget::magicValue`,将`cacheValid`视为`false`,执行这两个昂贵的计算,并将它们的和分配给`cachedValue`。
+ 此时,第二个线程调用`Widget::magicValue`,也将`cacheValid`视为`false`,因此执行刚才完成的第一个线程相同的计算。(这里的“第二个线程”实际上可能是其他**几个**线程。)
这种行为与使用缓存的目的背道而驰。将`cachedValue`和`CacheValid`的赋值顺序交换可以解决这个问题,但结果会更糟:
```c++
class Widget {
public:
…
int magicValue() const
{
if (cacheValid) return cachedValue;
else {
auto val1 = expensiveComputation1();
auto val2 = expensiveComputation2();
cacheValid = true; //第一步
return cachedValue = val1 + val2; //第二步
}
}
…
}
```
假设`cacheValid`是false,那么:
+ 一个线程调用`Widget::magicValue`,刚执行完将`cacheValid`设置`true`的语句。
+ 在这时,第二个线程调用`Widget::magicValue`,检查`cacheValid`。看到它是`true`,就返回`cacheValue`,即使第一个线程还没有给它赋值。因此返回的值是不正确的。
这里有一个坑。对于需要同步的是单个的变量或者内存位置,使用`std::atomic`就足够了。不过,一旦你需要对两个以上的变量或内存位置作为一个单元来操作的话,就应该使用互斥量。对于`Widget::magicValue`是这样的。
```c++
class Widget {
public:
…
int magicValue() const
{
std::lock_guard guard(m); //锁定m
if (cacheValid) return cachedValue;
else {
auto val1 = expensiveComputation1();
auto val2 = expensiveComputation2();
cachedValue = val1 + val2;
cacheValid = true;
return cachedValue;
}
} //解锁m
…
private:
mutable std::mutex m;
mutable int cachedValue; //不再用atomic
mutable bool cacheValid{ false }; //不再用atomic
};
```
现在,这个条款是基于,多个线程可以同时在一个对象上执行一个`const`成员函数这个假设的。如果你不是在这种情况下编写一个`const`成员函数——你可以**保证**在一个对象上永远不会有多个线程执行该成员函数——该函数的线程安全是无关紧要的。比如,为独占单线程使用而设计的类的成员函数是否线程安全并不重要。在这种情况下,你可以避免因使用互斥量和`std::atomics`所消耗的资源,以及包含它们的类~~只能使用移动语义~~(译者注:既不能移动也不能复制)带来的副作用。然而,这种线程无关的情况越来越少见,而且很可能会越来越少。可以肯定的是,`const`成员函数应支持并发执行,这就是为什么你应该确保`const`成员函数是线程安全的。
**请记住:**
+ 确保`const`成员函数线程安全,除非你**确定**它们永远不会在并发上下文(*concurrent context*)中使用。
+ 使用`std::atomic`变量可能比互斥量提供更好的性能,但是它只适合操作单个变量或内存位置。
================================================
FILE: src/3.MovingToModernCpp/item17.md
================================================
## 条款十七:理解特殊成员函数的生成
**Item 17: Understand special member function generation**
在C++术语中,**特殊成员函数**是指C++自己生成的函数。C++98有四个:默认构造函数,析构函数,拷贝构造函数,拷贝赋值运算符。当然在这里有些细则要注意。这些函数仅在需要的时候才生成,比如某个代码使用它们但是它们没有在类中明确声明。默认构造函数仅在类完全没有构造函数的时候才生成。(防止编译器为某个类生成构造函数,但是你希望那个构造函数有参数)生成的特殊成员函数是隐式public且`inline`,它们是非虚的,除非相关函数是在派生类中的析构函数,派生类继承了有虚析构函数的基类。在这种情况下,编译器为派生类生成的析构函数是虚的。
但是你早就知道这些了。好吧好吧,都说古老的历史:美索不达米亚,商朝,FORTRAN,C++98。但是时代改变了,C++生成特殊成员的规则也改变了。要留意这些新规则,知道什么时候编译器会悄悄地向你的类中添加成员函数,因为没有什么比这件事对C++高效编程更重要。
C++11特殊成员函数俱乐部迎来了两位新会员:移动构造函数和移动赋值运算符。它们的签名是:
```cpp
class Widget {
public:
…
Widget(Widget&& rhs); //移动构造函数
Widget& operator=(Widget&& rhs); //移动赋值运算符
…
};
```
掌控它们生成和行为的规则类似于拷贝系列。移动操作仅在需要的时候生成,如果生成了,就会对类的non-static数据成员执行逐成员的移动。那意味着移动构造函数根据`rhs`参数里面对应的成员移动构造出新non-static部分,移动赋值运算符根据参数里面对应的non-static成员移动赋值。移动构造函数也移动构造基类部分(如果有的话),移动赋值运算符也是移动赋值基类部分。
现在,当我对一个数据成员或者基类使用移动构造或者移动赋值时,没有任何保证移动一定会真的发生。逐成员移动,实际上,更像是逐成员移动**请求**,因为对**不可移动类型**(即对移动操作没有特殊支持的类型,比如大部分C++98传统类)使用“移动”操作实际上执行的是拷贝操作。逐成员移动的核心是对对象使用`std::move`,然后函数决议时会选择执行移动还是拷贝操作。[Item23](../5.RRefMovSemPerfForw/item23.md)包括了这个操作的细节。本条款中,简单记住如果支持移动就会逐成员移动类成员和基类成员,如果不支持移动就执行拷贝操作就好了。
像拷贝操作情况一样,如果你自己声明了移动操作,编译器就不会生成。然而它们生成的精确条件与拷贝操作的条件有点不同。
两个拷贝操作是独立的:声明一个不会限制编译器生成另一个。所以如果你声明一个拷贝构造函数,但是没有声明拷贝赋值运算符,如果写的代码用到了拷贝赋值,编译器会帮助你生成拷贝赋值运算符。同样的,如果你声明拷贝赋值运算符但是没有拷贝构造函数,代码用到拷贝构造函数时编译器就会生成它。上述规则在C++98和C++11中都成立。
两个移动操作不是相互独立的。如果你声明了其中一个,编译器就不再生成另一个。如果你给类声明了,比如,一个移动构造函数,就表明对于移动操作应怎样实现,与编译器应生成的默认逐成员移动有些区别。如果逐成员移动构造有些问题,那么逐成员移动赋值同样也可能有问题。所以声明移动构造函数阻止移动赋值运算符的生成,声明移动赋值运算符同样阻止编译器生成移动构造函数。
再进一步,如果一个类显式声明了拷贝操作,编译器就不会生成移动操作。这种限制的解释是如果声明拷贝操作(构造或者赋值)就暗示着平常拷贝对象的方法(逐成员拷贝)不适用于该类,编译器会明白如果逐成员拷贝对拷贝操作来说不合适,逐成员移动也可能对移动操作来说不合适。
这是另一个方向。声明移动操作(构造或赋值)使得编译器禁用拷贝操作。(编译器通过给拷贝操作加上*delete*来保证,参见[Item11](../3.MovingToModernCpp/item11.md)。)(译注:禁用的是自动生成的拷贝操作,对于用户声明的拷贝操作不受影响)毕竟,如果逐成员移动对该类来说不合适,也没有理由指望逐成员拷贝操作是合适的。听起来会破坏C++98的某些代码,因为C++11中拷贝操作可用的条件比C++98更受限,但事实并非如此。C++98的代码没有移动操作,因为C++98中没有移动对象这种概念。只有一种方法能让老代码使用用户声明的移动操作,那就是使用C++11标准然后添加这些操作,使用了移动语义的类必须接受C++11特殊成员函数生成规则的限制。
也许你早已听过_Rule of Three_规则。这个规则告诉我们如果你声明了拷贝构造函数,拷贝赋值运算符,或者析构函数三者之一,你应该也声明其余两个。它来源于长期的观察,即用户接管拷贝操作的需求几乎都是因为该类会做其他资源的管理,这也几乎意味着(1)无论哪种资源管理如果在一个拷贝操作内完成,也应该在另一个拷贝操作内完成(2)类的析构函数也需要参与资源的管理(通常是释放)。通常要管理的资源是内存,这也是为什么标准库里面那些管理内存的类(如会动态内存管理的STL容器)都声明了“*the big three*”:拷贝构造,拷贝赋值和析构。
*Rule of Three*带来的后果就是只要出现用户定义的析构函数就意味着简单的逐成员拷贝操作不适用于该类。那意味着如果一个类声明了析构,拷贝操作可能不应该自动生成,因为它们做的事情可能是错误的。在C++98提出的时候,上述推理没有得倒足够的重视,所以C++98用户声明析构函数不会左右编译器生成拷贝操作的意愿。C++11中情况仍然如此,但仅仅是因为限制拷贝操作生成的条件会破坏老代码。
*Rule of Three*规则背后的解释依然有效,再加上对声明拷贝操作阻止移动操作隐式生成的观察,使得C++11不会为那些有用户定义的析构函数的类生成移动操作。
所以仅当下面条件成立时才会生成移动操作(当需要时):
+ 类中没有拷贝操作
+ 类中没有移动操作
+ 类中没有用户定义的析构
有时,类似的规则也会扩展至拷贝操作上面,C++11抛弃了已声明拷贝操作或析构函数的类的拷贝操作的自动生成。这意味着如果你的某个声明了析构或者拷贝的类依赖自动生成的拷贝操作,你应该考虑升级这些类,消除依赖。假设编译器生成的函数行为是正确的(即逐成员拷贝类non-static数据是你期望的行为),你的工作很简单,C++11的`= default`就可以表达你想做的:
```cpp
class Widget {
public:
…
~Widget(); //用户声明的析构函数
… //默认拷贝构造函数
Widget(const Widget&) = default; //的行为还可以
Widget& //默认拷贝赋值运算符
operator=(const Widget&) = default; //的行为还可以
…
};
```
这种方法通常在多态基类中很有用,即通过操作的是哪个派生类对象来定义接口。多态基类通常有一个虚析构函数,因为如果它们非虚,一些操作(比如通过一个基类指针或者引用对派生类对象使用`delete`或者`typeid`)会产生未定义或错误结果。除非类继承了一个已经是*virtual*的析构函数,否则要想析构函数为虚函数的唯一方法就是加上`virtual`关键字。通常,默认实现是对的,`= default`是一个不错的方式表达默认实现。然而用户声明的析构函数会抑制编译器生成移动操作,所以如果该类需要具有移动性,就为移动操作加上`= default`。声明移动会抑制拷贝生成,所以如果拷贝性也需要支持,再为拷贝操作加上`= default`:
```cpp
class Base {
public:
virtual ~Base() = default; //使析构函数virtual
Base(Base&&) = default; //支持移动
Base& operator=(Base&&) = default;
Base(const Base&) = default; //支持拷贝
Base& operator=(const Base&) = default;
…
};
```
实际上,就算编译器乐于为你的类生成拷贝和移动操作,生成的函数也如你所愿,你也应该手动声明它们然后加上`= default`。这看起来比较多余,但是它让你的意图更明确,也能帮助你避免一些微妙的bug。比如,你有一个类来表示字符串表,即一种支持使用整数ID快速查找字符串值的数据结构:
```cpp
class StringTable {
public:
StringTable() {}
… //插入、删除、查找等函数,但是没有拷贝/移动/析构功能
private:
std::map values;
};
```
假设这个类没有声明拷贝操作,没有移动操作,也没有析构,如果它们被用到编译器会自动生成。没错,很方便。
后来需要在对象构造和析构中打日志,增加这种功能很简单:
```cpp
class StringTable {
public:
StringTable()
{ makeLogEntry("Creating StringTable object"); } //增加的
~StringTable() //也是增加的
{ makeLogEntry("Destroying StringTable object"); }
… //其他函数同之前一样
private:
std::map values; //同之前一样
};
```
看起来合情合理,但是声明析构有潜在的副作用:它阻止了移动操作的生成。然而,拷贝操作的生成是不受影响的。因此代码能通过编译,运行,也能通过功能(译注:即打日志的功能)测试。功能测试也包括移动功能,因为即使该类不支持移动操作,对该类的移动请求也能通过编译和运行。这个请求正如之前提到的,会转而由拷贝操作完成。它意味着对`StringTable`对象的移动实际上是对对象的拷贝,即拷贝里面的`std::map`对象。拷贝`std::map`对象很可能比移动慢**几个数量级**。简单的加个析构就引入了极大的性能问题!对拷贝和移动操作显式加个`= default`,问题将不再出现。
受够了我喋喋不休的讲述C++11拷贝移动规则了吧,你可能想知道什么时候我才会把注意力转入到剩下两个特殊成员函数,默认构造函数和析构函数。现在就是时候了,但是只有一句话,因为它们几乎没有改变:它们在C++98中是什么样,在C++11中就是什么样。
C++11对于特殊成员函数处理的规则如下:
+ **默认构造函数**:和C++98规则相同。仅当类不存在用户声明的构造函数时才自动生成。
+ **析构函数**:基本上和C++98相同;稍微不同的是现在析构默认`noexcept`(参见[Item14](../3.MovingToModernCpp/item14.md))。和C++98一样,仅当基类析构为虚函数时该类析构才为虚函数。
+ **拷贝构造函数**:和C++98运行时行为一样:逐成员拷贝non-static数据。仅当类没有用户定义的拷贝构造时才生成。如果类声明了移动操作它就是*delete*的。当用户声明了拷贝赋值或者析构,该函数自动生成已被废弃。
+ **拷贝赋值运算符**:和C++98运行时行为一样:逐成员拷贝赋值non-static数据。仅当类没有用户定义的拷贝赋值时才生成。如果类声明了移动操作它就是*delete*的。当用户声明了拷贝构造或者析构,该函数自动生成已被废弃。
+ **移动构造函数**和**移动赋值运算符**:都对非static数据执行逐成员移动。仅当类没有用户定义的拷贝操作,移动操作或析构时才自动生成。
注意没有“成员函数**模版**阻止编译器生成特殊成员函数”的规则。这意味着如果`Widget`是这样:
```cpp
class Widget {
…
template //从任何东西构造Widget
Widget(const T& rhs);
template //从任何东西赋值给Widget
Widget& operator=(const T& rhs);
…
};
```
编译器仍会生成移动和拷贝操作(假设正常生成它们的条件满足),即使可以模板实例化产出拷贝构造和拷贝赋值运算符的函数签名。(当`T`为`Widget`时。)很可能你会觉得这是一个不值得承认的边缘情况,但是我提到它是有道理的,[Item26](../5.RRefMovSemPerfForw/item26.md)将会详细讨论它可能带来的后果。
**请记住:**
+ 特殊成员函数是编译器可能自动生成的函数:默认构造函数,析构函数,拷贝操作,移动操作。
+ 移动操作仅当类没有显式声明移动操作,拷贝操作,析构函数时才自动生成。
+ 拷贝构造函数仅当类没有显式声明拷贝构造函数时才自动生成,并且如果用户声明了移动操作,拷贝构造就是*delete*。拷贝赋值运算符仅当类没有显式声明拷贝赋值运算符时才自动生成,并且如果用户声明了移动操作,拷贝赋值运算符就是*delete*。当用户声明了析构函数,拷贝操作的自动生成已被废弃。
+ 成员函数模板不抑制特殊成员函数的生成。
================================================
FILE: src/3.MovingToModernCpp/item7.md
================================================
# 第3章 移步现代C++
**CHAPTER 3 Moving to Modern C++**
说起知名的特性,C++11/14有一大堆可以吹的东西,`auto`,智能指针(*smart pointer*),移动语义(*move semantics*),*lambda*,并发(*concurrency*)——每个都是如此的重要,这章将覆盖这些内容。掌握这些特性是必要的,要想成为高效率的现代C++程序员需要小步迈进。在从C++98小步迈进到现代C++过程中遇到的每个问题,本章都会一一回答。你什么时候应该用{}而不是()创建对象?为什么别名(*alias*)声明比`typedef`好?`constexpr`和`const`有什么不同?常量(`const`)成员函数和线程安全有什么关系?这个列表越列越多,这章将会逐个回答这些问题。
## 条款七:区别使用`()`和`{}`创建对象
**Item 7: Distinguish between `()` and `{}` when creating objects**
取决于你看问题的角度,C++11对象初始化的语法可能会让你觉得丰富的让人难以选择,亦或是乱的一塌糊涂。一般来说,初始化值要用圆括号()或者花括号{}括起来,或者放到等号"="的右边:
````cpp
int x(0); //使用圆括号初始化
int y = 0; //使用"="初始化
int z{ 0 }; //使用花括号初始化
````
在很多情况下,你可以使用"="和花括号的组合:
````cpp
int z = { 0 }; //使用"="和花括号
````
在这个条款的剩下部分,我通常会忽略"="和花括号组合初始化的语法,因为C++通常把它视作和只有花括号一样。
“乱的一塌糊涂”是指在初始化中使用"="可能会误导C++新手,使他们以为这里发生了赋值运算,然而实际并没有。对于像`int`这样的内置类型,研究两者区别就像在做学术,但是对于用户定义的类型而言,区别赋值运算符和初始化就非常重要了,因为它们涉及不同的函数调用:
````cpp
Widget w1; //调用默认构造函数
Widget w2 = w1; //不是赋值运算,调用拷贝构造函数
w1 = w2; //是赋值运算,调用拷贝赋值运算符(copy operator=)
````
甚至对于一些初始化语法,在一些情况下C++98没有办法表达预期的初始化行为。举个例子,要想直接创建并初始化一个存放一些特殊值的STL容器是不可能的(比如1,3,5)。
C++11使用统一初始化(*uniform initialization*)来整合这些混乱且不适于所有情景的初始化语法,所谓统一初始化是指在任何涉及初始化的地方都使用单一的初始化语法。
它基于花括号,出于这个原因我更喜欢称之为括号初始化。(**译注:注意,这里的括号初始化指的是花括号初始化,在没有歧义的情况下下文的括号初始化指的都是用花括号进行初始化;当与圆括号初始化同时存在并可能产生歧义时我会直接指出。**)统一初始化是一个概念上的东西,而括号初始化是一个具体语法结构。
括号初始化让你可以表达以前表达不出的东西。使用花括号,创建并指定一个容器的初始元素变得很容易:
````cpp
std::vector v{ 1, 3, 5 }; //v初始内容为1,3,5
````
括号初始化也能被用于为非静态数据成员指定默认初始值。C++11允许"="初始化不加花括号也拥有这种能力:
````cpp
class Widget{
…
private:
int x{ 0 }; //没问题,x初始值为0
int y = 0; //也可以
int z(0); //错误!
}
````
另一方面,不可拷贝的对象(例如`std::atomic`——见[Item40](../7.TheConcurrencyAPI/item40.md))可以使用花括号初始化或者圆括号初始化,但是不能使用"="初始化:
````cpp
std::atomic ai1{ 0 }; //没问题
std::atomic ai2(0); //没问题
std::atomic ai3 = 0; //错误!
````
因此我们很容易理解为什么括号初始化又叫统一初始化,在C++中这三种方式都被看做是初始化表达式,但是只有花括号任何地方都能被使用。
括号表达式还有一个少见的特性,即它不允许内置类型间隐式的变窄转换(*narrowing conversion*)。如果一个使用了括号初始化的表达式的值,不能保证由被初始化的对象的类型来表示,代码就不会通过编译:
````cpp
double x, y, z;
int sum1{ x + y + z }; //错误!double的和可能不能表示为int
````
使用圆括号和"="的初始化不检查是否转换为变窄转换,因为由于历史遗留问题它们必须要兼容老旧代码:
````cpp
int sum2(x + y +z); //可以(表达式的值被截为int)
int sum3 = x + y + z; //同上
````
另一个值得注意的特性是括号表达式对于C++最令人头疼的解析问题有天生的免疫性。(译注:所谓最令人头疼的解析即*most vexing parse*,更多信息请参见[https://en.wikipedia.org/wiki/Most_vexing_parse](https://en.wikipedia.org/wiki/Most_vexing_parse)。)C++规定任何*可以被解析*为一个声明的东西*必须被解析*为声明。这个规则的副作用是让很多程序员备受折磨:他们可能想创建一个使用默认构造函数构造的对象,却不小心变成了函数声明。问题的根源是如果你调用带参构造函数,你可以这样做:
````cpp
Widget w1(10); //使用实参10调用Widget的一个构造函数
````
但是如果你尝试使用相似的语法调用`Widget`无参构造函数,它就会变成函数声明:
````cpp
Widget w2(); //最令人头疼的解析!声明一个函数w2,返回Widget
````
由于函数声明中形参列表不能带花括号,所以使用花括号初始化表明你想调用默认构造函数构造对象就没有问题:
````cpp
Widget w3{}; //调用没有参数的构造函数构造对象
````
关于括号初始化还有很多要说的。它的语法能用于各种不同的上下文,它防止了隐式的变窄转换,而且对于C++最令人头疼的解析也天生免疫。既然好到这个程度那为什么这个条款不叫“优先考虑括号初始化语法”呢?
括号初始化的缺点是有时它有一些令人惊讶的行为。这些行为使得括号初始化、`std::initializer_list`和构造函数参与重载决议时本来就不清不楚的暧昧关系进一步混乱。把它们放到一起会让看起来应该左转的代码右转。举个例子,[Item2](../1.DeducingTypes/item2.md)解释了当`auto`声明的变量使用花括号初始化,变量类型会被推导为`std::initializer_list`,但是使用相同内容的其他初始化方式会产生更符合直觉的结果。所以,你越喜欢用`auto`,你就越不能用括号初始化。
在构造函数调用中,只要不包含`std::initializer_list`形参,那么花括号初始化和圆括号初始化都会产生一样的结果:
````cpp
class Widget {
public:
Widget(int i, bool b); //构造函数未声明
Widget(int i, double d); //std::initializer_list这个形参
…
};
Widget w1(10, true); //调用第一个构造函数
Widget w2{10, true}; //也调用第一个构造函数
Widget w3(10, 5.0); //调用第二个构造函数
Widget w4{10, 5.0}; //也调用第二个构造函数
````
然而,如果有一个或者多个构造函数的声明包含一个`std::initializer_list`形参,那么使用括号初始化语法的调用更倾向于选择带`std::initializer_list`的那个构造函数。如果编译器遇到一个括号初始化并且有一个带std::initializer_list的构造函数,那么它一定会选择该构造函数。如果上面的`Widget`类有一个`std::initializer_list`作为参数的构造函数,就像这样:
````cpp
class Widget {
public:
Widget(int i, bool b); //同上
Widget(int i, double d); //同上
Widget(std::initializer_list il); //新添加的
…
};
````
`w2`和`w4`将会使用新添加的构造函数,即使另一个非`std::initializer_list`构造函数和实参更匹配:
````cpp
Widget w1(10, true); //使用圆括号初始化,同之前一样
//调用第一个构造函数
Widget w2{10, true}; //使用花括号初始化,但是现在
//调用带std::initializer_list的构造函数
//(10 和 true 转化为long double)
Widget w3(10, 5.0); //使用圆括号初始化,同之前一样
//调用第二个构造函数
Widget w4{10, 5.0}; //使用花括号初始化,但是现在
//调用带std::initializer_list的构造函数
//(10 和 5.0 转化为long double)
````
甚至普通构造函数和移动构造函数都会被带`std::initializer_list`的构造函数劫持:
````cpp
class Widget {
public:
Widget(int i, bool b); //同之前一样
Widget(int i, double d); //同之前一样
Widget(std::initializer_list il); //同之前一样
operator float() const; //转换为float
…
};
Widget w5(w4); //使用圆括号,调用拷贝构造函数
Widget w6{w4}; //使用花括号,调用std::initializer_list构造
//函数(w4转换为float,float转换为double)
Widget w7(std::move(w4)); //使用圆括号,调用移动构造函数
Widget w8{std::move(w4)}; //使用花括号,调用std::initializer_list构造
//函数(与w6相同原因)
````
编译器一遇到括号初始化就选择带`std::initializer_list`的构造函数的决心是如此强烈,以至于就算带`std::initializer_list`的构造函数不能被调用,它也会硬选。
````cpp
class Widget {
public:
Widget(int i, bool b); //同之前一样
Widget(int i, double d); //同之前一样
Widget(std::initializer_list il); //现在元素类型为bool
… //没有隐式转换函数
};
Widget w{10, 5.0}; //错误!要求变窄转换
````
这里,编译器会直接忽略前面两个构造函数(其中第二个构造函数是所有实参类型的最佳匹配),然后尝试调用`std::initializer_list`构造函数。调用这个函数将会把`int(10)`和`double(5.0)`转换为`bool`,由于会产生变窄转换(`bool`不能准确表示其中任何一个值),括号初始化拒绝变窄转换,所以这个调用无效,代码无法通过编译。
只有当没办法把括号初始化中实参的类型转化为`std::initializer_list`时,编译器才会回到正常的函数决议流程中。比如我们在构造函数中用`std::initializer_list`代替`std::initializer_list`,这时非`std::initializer_list`构造函数将再次成为函数决议的候选者,因为没有办法把`int`和`bool`转换为`std::string`:
````cpp
class Widget {
public:
Widget(int i, bool b); //同之前一样
Widget(int i, double d); //同之前一样
//现在std::initializer_list元素类型为std::string
Widget(std::initializer_list il);
… //没有隐式转换函数
};
Widget w1(10, true); // 使用圆括号初始化,调用第一个构造函数
Widget w2{10, true}; // 使用花括号初始化,现在调用第一个构造函数
Widget w3(10, 5.0); // 使用圆括号初始化,调用第二个构造函数
Widget w4{10, 5.0}; // 使用花括号初始化,现在调用第二个构造函数
````
代码的行为和我们刚刚的论述如出一辙。这里还有一个有趣的[边缘情况](https://en.wikipedia.org/wiki/Edge_case)。假如你使用的花括号初始化是空集,并且你欲构建的对象有默认构造函数,也有`std::initializer_list`构造函数。你的空的花括号意味着什么?如果它们意味着没有实参,就该使用默认构造函数,但如果它意味着一个空的`std::initializer_list`,就该调用`std::initializer_list`构造函数。
最终会调用默认构造函数。空的花括号意味着没有实参,不是一个空的`std::initializer_list`:
````cpp
class Widget {
public:
Widget(); //默认构造函数
Widget(std::initializer_list il); //std::initializer_list构造函数
… //没有隐式转换函数
};
Widget w1; //调用默认构造函数
Widget w2{}; //也调用默认构造函数
Widget w3(); //最令人头疼的解析!声明一个函数
````
如果你**想**用空`std::initializer`来调用`std::initializer_list`构造函数,你就得创建一个空花括号作为函数实参——把空花括号放在圆括号或者另一个花括号内来界定你想传递的东西。
````cpp
Widget w4({}); //使用空花括号列表调用std::initializer_list构造函数
Widget w5{{}}; //同上
````
此时,括号初始化,`std::initializer_list`和构造函数重载的晦涩规则就会一下子涌进你的脑袋,你可能会想研究半天这些东西在你的日常编程中到底占多大比例。可能比你想象的要有用。因为`std::vector`作为受众之一会直接受到影响。`std::vector`有一个非`std::initializer_list`构造函数允许你去指定容器的初始大小,以及使用一个值填满你的容器。但它也有一个`std::initializer_list`构造函数允许你使用花括号里面的值初始化容器。如果你创建一个数值类型的`std::vector`(比如`std::vector`),然后你传递两个实参,把这两个实参放到圆括号和放到花括号中有天壤之别:
````cpp
std::vector v1(10, 20); //使用非std::initializer_list构造函数
//创建一个包含10个元素的std::vector,
//所有的元素的值都是20
std::vector v2{10, 20}; //使用std::initializer_list构造函数
//创建包含两个元素的std::vector,
//元素的值为10和20
````
让我们回到之前的话题。从以上讨论中我们得出两个重要结论。第一,作为一个类库作者,你需要意识到如果一堆重载的构造函数中有一个或者多个含有`std::initializer_list`形参,用户代码如果使用了括号初始化,可能只会看到你`std::initializer_list`版本的重载的构造函数。因此,你最好把你的构造函数设计为不管用户是使用圆括号还是使用花括号进行初始化都不会有什么影响。换句话说,了解了`std::vector`设计缺点后,你以后设计类的时候应该避免诸如此类的问题。
这里的暗语是如果一个类没有`std::initializer_list`构造函数,然后你添加一个,用户代码中如果使用括号初始化,可能会发现过去被决议为非`std::initializer_list`构造函数而现在被决议为新的函数。当然,这种事情也可能发生在你添加一个函数到那堆重载函数的时候:过去被决议为旧的重载函数而现在调用了新的函数。`std::initializer_list`重载不会和其他重载函数比较,它直接盖过了其它重载函数,其它重载函数几乎不会被考虑。所以如果你要加入`std::initializer_list`构造函数,请三思而后行。
第二,作为一个类库使用者,你必须认真的在花括号和圆括号之间选择一个来创建对象。大多数开发者都使用其中一种作为默认情况,只有当他们不能使用这种的时候才会考虑另一种。默认使用花括号初始化的开发者主要被适用面广、禁止变窄转换、免疫C++最令人头疼的解析这些优点所吸引。这些开发者知道在一些情况下(比如给定一个容器大小和一个初始值创建`std::vector`)要使用圆括号。默认使用圆括号初始化的开发者主要被C++98语法一致性、避免`std::initializer_list`自动类型推导、避免不会不经意间调用`std::initializer_list`构造函数这些优点所吸引。这些开发者也承认有时候只能使用花括号(比如创建一个包含着特定值的容器)。关于花括号初始化和圆括号初始化哪种更好大家没有达成一致,所以我的建议是选择一种并坚持使用它。
如果你是一个模板的作者,花括号和圆括号创建对象就更麻烦了。通常不能知晓哪个会被使用。举个例子,假如你想创建一个接受任意数量的参数来创建的对象。使用可变参数模板(*variadic template*)可以非常简单的解决:
````cpp
template //要使用的实参的类型
void doSomeWork(Ts&&... params)
{
create local T object from params...
…
}
````
在现实中我们有两种方式实现这个伪代码(关于`std::forward`请参见[Item25](../5.RRefMovSemPerfForw/item25.md)):
````cpp
T localObject(std::forward(params)...); //使用圆括号
T localObject{std::forward(params)...}; //使用花括号
````
考虑这样的调用代码:
````cpp
std::vector v;
…
doSomeWork>(10, 20);
````
如果`doSomeWork`创建`localObject`时使用的是圆括号,`std::vector`就会包含10个元素。如果`doSomeWork`创建`localObject`时使用的是花括号,`std::vector`就会包含2个元素。哪个是正确的?`doSomeWork`的作者不知道,只有调用者知道。
这正是标准库函数`std::make_unique`和`std::make_shared`(参见[Item21](../4.SmartPointers/item21.md))面对的问题。它们的解决方案是使用圆括号,并被记录在文档中作为接口的一部分。(注:更灵活的设计——允许调用者决定从模板来的函数应该使用圆括号还是花括号——是有可能的。详情参见[Andrzej’s C++ blog](http://akrzemi1.wordpress.com/)在2013年6月5日的文章,“[Intuitive interface — Part I.](http://akrzemi1.wordpress.com/2013/06/05/intuitive-interface-part-i/)”)
**请记住:**
+ 花括号初始化是最广泛使用的初始化语法,它防止变窄转换,并且对于C++最令人头疼的解析有天生的免疫性
+ 在构造函数重载决议中,编译器会尽最大努力将括号初始化与`std::initializer_list`参数匹配,即便其他构造函数看起来是更好的选择
+ 对于数值类型的`std::vector`来说使用花括号初始化和圆括号初始化会造成巨大的不同
+ 在模板类选择使用圆括号初始化或使用花括号初始化创建对象是一个挑战。
================================================
FILE: src/3.MovingToModernCpp/item8.md
================================================
## 条款八:优先考虑`nullptr`而非`0`和`NULL`
**Item 8: Prefer `nullptr` to `0` and `NULL`**
你看这样对不对:字面值`0`是一个`int`不是指针。如果C++发现在当前上下文只能使用指针,它会很不情愿的把`0`解释为指针,但是那是最后的退路。一般来说C++的解析策略是把`0`看做`int`而不是指针。
实际上,`NULL`也是这样的。但在`NULL`的实现细节有些不确定因素,因为实现被允许给`NULL`一个除了`int`之外的整型类型(比如`long`)。这不常见,但也算不上问题所在。这里的问题不是`NULL`没有一个确定的类型,而是`0`和`NULL`都不是指针类型。
在C++98中,对指针类型和整型进行重载意味着可能导致奇怪的事情。如果给下面的重载函数传递`0`或`NULL`,它们绝不会调用指针版本的重载函数:
````cpp
void f(int); //三个f的重载函数
void f(bool);
void f(void*);
f(0); //调用f(int)而不是f(void*)
f(NULL); //可能不会被编译,一般来说调用f(int),
//绝对不会调用f(void*)
````
而`f(NULL)`的不确定行为是由`NULL`的实现不同造成的。如果`NULL`被定义为`0L`(指的是`0`为`long`类型),这个调用就具有二义性,因为从`long`到`int`的转换或从`long`到`bool`的转换或`0L`到`void*`的转换都同样好。有趣的是源代码**表现出**的意思(“我使用空指针`NULL`调用`f`”)和**实际表达出**的意思(“我是用整型数据而不是空指针调用`f`”)是相矛盾的。这种违反直觉的行为导致C++98程序员都将避开同时重载指针和整型作为编程准则(译注:请务必注意结合上下文使用这条规则)。在C++11中这个编程准则也有效,因为尽管我这个条款建议使用`nullptr`,可能很多程序员还是会继续使用`0`或`NULL`,哪怕`nullptr`是更好的选择。
`nullptr`的优点是它不是整型。老实说它也不是一个指针类型,但是你可以把它认为是**所有**类型的指针。`nullptr`的真正类型是`std::nullptr_t`,在一个完美的循环定义以后,`std::nullptr_t`又被定义为`nullptr`。`std::nullptr_t`可以隐式转换为指向任何内置类型的指针,这也是为什么`nullptr`表现得像所有类型的指针。
使用`nullptr`调用`f`将会调用`void*`版本的重载函数,因为`nullptr`不能被视作任何整型:
````cpp
f(nullptr); //调用重载函数f的f(void*)版本
````
使用`nullptr`代替`0`和`NULL`可以避开了那些令人奇怪的函数重载决议,这不是它的唯一优势。它也可以使代码表意明确,尤其是当涉及到与`auto`声明的变量一起使用时。举个例子,假如你在一个代码库中遇到了这样的代码:
````cpp
auto result = findRecord( /* arguments */ );
if (result == 0) {
…
}
````
如果你不知道`findRecord`返回了什么(或者不能轻易的找出),那么你就不太清楚到底`result`是一个指针类型还是一个整型。毕竟,`0`(用来测试`result`的值的那个)也可以像我们之前讨论的那样被解析。但是换一种假设如果你看到这样的代码:
````cpp
auto result = findRecord( /* arguments */ );
if (result == nullptr) {
…
}
````
这就没有任何歧义:`result`的结果一定是指针类型。
当模板出现时`nullptr`就更有用了。假如你有一些函数只能被合适的已锁互斥量调用。每个函数都有一个不同类型的指针:
````cpp
int f1(std::shared_ptr spw); //只能被合适的
double f2(std::unique_ptr upw); //已锁互斥量
bool f3(Widget* pw); //调用
````
如果这样传递空指针:
````cpp
std::mutex f1m, f2m, f3m; //用于f1,f2,f3函数的互斥量
using MuxGuard = //C++11的typedef,参见Item9
std::lock_guard;
…
{
MuxGuard g(f1m); //为f1m上锁
auto result = f1(0); //向f1传递0作为空指针
} //解锁
…
{
MuxGuard g(f2m); //为f2m上锁
auto result = f2(NULL); //向f2传递NULL作为空指针
} //解锁
…
{
MuxGuard g(f3m); //为f3m上锁
auto result = f3(nullptr); //向f3传递nullptr作为空指针
} //解锁
````
令人遗憾前两个调用没有使用`nullptr`,但是代码可以正常运行,这也许对一些东西有用。但是重复的调用代码——为互斥量上锁,调用函数,解锁互斥量——更令人遗憾。它让人很烦。模板就是被设计于减少重复代码,所以让我们模板化这个调用流程:
````cpp
template
auto lockAndCall(FuncType func,
MuxType& mutex,
PtrType ptr) -> decltype(func(ptr))
{
MuxGuard g(mutex);
return func(ptr);
}
````
如果你对函数返回类型(`auto ... -> decltype(func(ptr))`)感到困惑不解,[Item3](../1.DeducingTypes/item3.md)可以帮助你。在C++14中代码的返回类型还可以被简化为`decltype(auto)`:
````cpp
template
decltype(auto) lockAndCall(FuncType func, //C++14
MuxType& mutex,
PtrType ptr)
{
MuxGuard g(mutex);
return func(ptr);
}
````
可以写这样的代码调用`lockAndCall`模板(两个版本都可):
````cpp
auto result1 = lockAndCall(f1, f1m, 0); //错误!
...
auto result2 = lockAndCall(f2, f2m, NULL); //错误!
...
auto result3 = lockAndCall(f3, f3m, nullptr); //没问题
````
代码虽然可以这样写,但是就像注释中说的,前两个情况不能通过编译。在第一个调用中存在的问题是当`0`被传递给`lockAndCall`模板,模板类型推导会尝试去推导实参类型,`0`的类型总是`int`,所以这就是这次调用`lockAndCall`实例化出的`ptr`的类型。不幸的是,这意味着`lockAndCall`中`func`会被`int`类型的实参调用,这与`f1`期待的`std::shared_ptr`形参不符。传递`0`给`lockAndCall`本来想表示空指针,但是实际上得到的一个普通的`int`。把`int`类型看做`std::shared_ptr`类型给`f1`自然是一个类型错误。在模板`lockAndCall`中使用`0`之所以失败是因为在模板中,传给的是`int`但实际上函数期待的是一个`std::shared_ptr`。
第二个使用`NULL`调用的分析也是一样的。当`NULL`被传递给`lockAndCall`,形参`ptr`被推导为整型(译注:由于依赖于具体实现所以不一定是整数类型,所以用整型泛指`int`,`long`等类型),然后当`ptr`——一个`int`或者类似`int`的类型——传递给`f2`的时候就会出现类型错误,`f2`期待的是`std::unique_ptr`。
然而,使用`nullptr`是调用没什么问题。当`nullptr`传给`lockAndCall`时,`ptr`被推导为`std::nullptr_t`。当`ptr`被传递给`f3`的时候,隐式转换使`std::nullptr_t`转换为`Widget*`,因为`std::nullptr_t`可以隐式转换为任何指针类型。
模板类型推导将`0`和`NULL`推导为一个错误的类型(即它们的实际类型,而不是作为空指针的隐含意义),这就导致在当你想要一个空指针时,它们的替代品`nullptr`很吸引人。使用`nullptr`,模板不会有什么特殊的转换。另外,使用`nullptr`不会让你受到同重载决议特殊对待`0`和`NULL`一样的待遇。当你想用一个空指针,使用`nullptr`,不用`0`或者`NULL`。
**记住**
+ 优先考虑`nullptr`而非`0`和`NULL`
+ 避免重载指针和整型
================================================
FILE: src/3.MovingToModernCpp/item9.md
================================================
## 条款九:优先考虑别名声明而非`typedef`
**Item 9: Prefer alias declarations to `typedef`**
我相信每个人都同意使用STL容器是个好主意,并且我希望[Item18](../4.SmartPointers/item18.md)能说服你让你觉得使用`std:unique_ptr`也是个好主意,但我猜没有人喜欢写上几次 `std::unique_ptr>`这样的类型,它可能会让你患上腕管综合征的风险大大增加。
避免上述医疗悲剧也很简单,引入`typedef`即可:
````cpp
typedef
std::unique_ptr>
UPtrMapSS;
````
但`typedef`是C++98的东西。虽然它可以在C++11中工作,但是C++11也提供了一个别名声明(*alias declaration*):
````cpp
using UPtrMapSS =
std::unique_ptr>;
````
由于这里给出的`typedef`和别名声明做的都是完全一样的事情,我们有理由想知道会不会出于一些技术上的原因两者有一个更好。
这里,在说它们之前我想提醒一下很多人都发现当声明一个函数指针时别名声明更容易理解:
````cpp
//FP是一个指向函数的指针的同义词,它指向的函数带有
//int和const std::string&形参,不返回任何东西
typedef void (*FP)(int, const std::string&); //typedef
//含义同上
using FP = void (*)(int, const std::string&); //别名声明
````
当然,两个结构都不是非常让人满意,没有人喜欢花大量的时间处理函数指针类型的别名(译注:指`FP`),所以至少在这里,没有一个吸引人的理由让你觉得别名声明比`typedef`好。
不过有一个地方使用别名声明吸引人的理由是存在的:模板。特别地,别名声明可以被模板化(这种情况下称为别名模板*alias template*s)但是`typedef`不能。这使得C++11程序员可以很直接的表达一些C++98中只能把`typedef`嵌套进模板化的`struct`才能表达的东西。考虑一个链表的别名,链表使用自定义的内存分配器,`MyAlloc`。使用别名模板,这真是太容易了:
````cpp
template //MyAllocList是
using MyAllocList = std::list>; //std::list>
//的同义词
MyAllocList lw; //用户代码
````
使用`typedef`,你就只能从头开始:
````cpp
template //MyAllocList是
struct MyAllocList { //std::list>
typedef std::list> type; //的同义词
};
MyAllocList::type lw; //用户代码
````
更糟糕的是,如果你想使用在一个模板内使用`typedef`声明一个链表对象,而这个对象又使用了模板形参,你就不得不在`typedef`前面加上`typename`:
````cpp
template
class Widget { //Widget含有一个
private: //MyAllocLIst