[errata very important](https://www.aristeia.com/BookErrata/emc++-errata.html)
> Argument, Actual Argument
> Parameter, Formal Parameter
## 一 类型推导
C++98有一套类型推导的规则:用于函数模板的规则。C++11修改了其中的一些规则并增加了两套规则,一套用于auto,一套用于decltype。C++14扩展了auto和decltype可能使用的范围。
#### Item 1 理解模板类型推导
------------------------------------------------
1 在编译期间,编译器使用expr进行两个类型推导:一个是针对T的,另一个是针对ParamType的。这两个类型通常是不同的,因为ParamType包含一些修饰,比如const和引用修饰符。
```
template<typename T> //从expr中推导T和ParamType
void f(ParamType param);
f(expr); //使用表达式调用f
```
下面是一个 T 与 ParamType 不同的例子:
```
template<typename T>
void f(const T& param);
```
2 T的类型推导不仅取决于expr的类型,也取决于ParamType的类型。总共分三种情况:
- ParamType是一个指针或引用,但不是通用引用(关于通用引用请参见Item24。在这里你只需要知道它存在,而且不同于左值引用和右值引用)
- ParamType一个通用引用
- ParamType既不是指针也不是引用
这里注意,三种情况是以 ParamType 来区分的。
2.1 对于第一种情况。 ParamType是一个指针或引用,但不是通用引用。这里要格外注意的是,T 的类型是 expr 的类型和 ParamType 进行模式匹配的结果。
- 如果expr的类型是一个引用,忽略引用部分
- 然后expr的类型与ParamType进行模式匹配来决定T
```
template<typename T>
void f(T& param); //param是一个引用
```
2.2 对于第二种情况。ParamType一个通用引用。这里面需要注意两点。第一,这是模板类型唯一一种T被推导为引用的情况。第二,ParamType 看起来是右值引用,但其实确实左值引用。
- 如果expr是左值,T和ParamType都会被推导为左值引用。这非常不寻常,第一,这是模板类型推导中唯一一种T被推导为引用的情况。第二,虽然ParamType被声明为右值引用类型,但是最后推导的结果是左值引用。
- 如果expr是右值,就使用正常的(也就是情景一)推导规则
```
template<typename T>
void f(T&& param); //param现在是一个通用引用类型
```
2.3 ParamType既不是指针也不是引用. 这里面比较值得注意的是,由于无论传递什么都将是一个变量的拷贝,所以 const 和 volatile 被忽略了。另外,这种情况 T 和 ParamType 应该会保持一致。
- 和之前一样,如果expr的类型是一个引用,忽略这个引用部分
- 如果忽略expr的引用性(reference-ness)之后,expr是一个const,那就再忽略const。如果它是volatile,也忽略volatile(volatile对象不常见,它通常用于驱动程序的开发中。关于volatile的细节请参见Item40)
```
template<typename T>
void f(T param); //以传值的方式处理param
```
3 但是考虑这样的情况,expr是一个const指针,指向const对象,expr通过传值传递给param. 由于变量本身通过传值传递,所以最终 T 和 paramType 都是 const char *。即:保留所指内容的常量性,而忽略变量本身的常量性。
```
template<typename T>
void f(T param); //仍然以传值的方式处理param
const char* const ptr = //ptr是一个常量指针,指向常量对象
"Fun with pointers";
f(ptr); //传递const char * const类型的实参
```
4 在模板类型推导时,数组名或者函数名实参会退化为指针,除非它们被用于初始化引用。对于数组名,会退化为 const char * 。如果是用来初始化引用,则能保留数组类型和长度。
数组初始化引用的能力可以用来推测数组大小:
```
//在编译期间返回一个数组大小的常量值(//数组形参没有名字,
//因为我们只关心数组的大小)
template<typename T, std::size_t N> //关于
constexpr std::size_t arraySize(T (&)[N]) noexcept //constexpr
{ //和noexcept
return N; //的信息
} //请看下面
```
函数的演示,数组同理:
```
void someFunc(int, double); //someFunc是一个函数,
//类型是void(int, double)
template<typename T>
void f1(T param); //传值给f1
template<typename T>
void f2(T & param); //传引用给f2
f1(someFunc); //param被推导为指向函数的指针,
//类型是void(*)(int, double)
f2(someFunc); //param被推导为指向函数的引用,
//类型是void(&)(int, double)
```
这里非常容易混乱,也有些奇怪。
首先,注意数组名和函数名的处理方式是一样的,会退化为指针。
其次,如果用来初始化引用,数组会变成 const char (&)[13],而函数会变成 void(&)(int, double)。
#### Item 2 理解auto类型推导
------------------------------------------------
1 在f的调用中,编译器使用 expr 推导 T 和 ParamType 的类型。当一个变量使用auto进行声明时,auto扮演了模板中T的角色,变量的类型说明符扮演了ParamType的角色。auto类型推导除了一个例外(我们很快就会讨论),其他情况都和模板类型推导一样。
2 当用auto声明的变量使用花括号进行初始化,auto类型推导推出的类型则为std::initializer_list,对于模板类型推导这样就行不通。如果这样的一个类型不能被成功推导(比如花括号里面包含的是不同类型的变量),编译器会拒绝这样的代码。因此auto类型推导和模板类型推导的真正区别在于,auto类型推导假定花括号表示std::initializer_list而模板类型推导不会这样(确切的说是不知道怎么办)。
3 C++14允许auto用于函数返回值并会被推导(参见Item3),而且C++14的lambda函数也允许在形参声明中使用auto。但是在这些情况下auto实际上使用模板类型推导的那一套规则在工作。因此,直接推导 std::initializer_list 仍然是行不通的。
#### Item 3 理解decltype
------------------------------------------------
decltype是一个奇怪的东西。给它一个名字或者表达式decltype就会告诉你这个名字或者表达式的类型。通常,它会精确的告诉你你想要的结果。但有时候它得出的结果也会让你挠头半天,最后只能求助网上问答或参考资料寻求启示。
*一个容器进行operator[]操作返回的类型取决于容器本身*
函数名称前面的auto不会做任何的类型推导工作。相反的,他只是暗示使用了C++11的尾置返回类型语法,即在函数形参列表后面使用一个”->“符号指出函数的返回类型,尾置返回类型的好处是我们可以在函数返回类型中使用函数形参相关的信息。
```
template<typename Container, typename Index> //最终的C++11版本
auto
authAndAccess(Container&& c, Index i)
->decltype(std::forward<Container>(c)[i])
{
authenticateUser();
return std::forward<Container>(c)[i];
}
template<typename Container, typename Index> //最终的C++14版本
decltype(auto)
authAndAccess(Container&& c, Index i)
{
authenticateUser();
return std::forward<Container>(c)[i];
}
```
C++ 14 的改进之处,有必要列一下:
1. C++11允许自动推导单一语句的lambda表达式的返回类型, C++14扩展到允许自动推导所有的lambda表达式和函数,甚至它们内含多条语句。
2. C++期望在某些情况下当类型被暗示时需要使用decltype类型推导的规则,C++14通过使用decltype(auto)说明符使得这成为可能。
*通用引用可以绑定左值和右值*
对于T类型的不是单纯的变量名的左值表达式,decltype总是产出T的引用即T&。这个确实很危险,注意不仅f2的返回类型不同于f1,而且它还引用了一个局部变量!
```
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&
}
```
#### Item 4 学会查看类型推导结果
------------------------------------------------
1. 类型推断可以从IDE看出,从编译器报错看出,从Boost TypeIndex库的使用看出。它们是有用的,但是作为本章结束语我想说它们根本不能替代你对Item1-3提到的类型推导的理解。
2. 这些工具可能既不准确也无帮助,所以理解C++类型推导规则才是最重要的
###### IDE
把鼠标放上去一般就可以了,你的代码必须或多或少的处于可编译状态,因为IDE之所以能提供这些信息是因为一个C++编译器(或者至少是前端中的一个部分)运行于IDE中。如果这个编译器对你的代码不能做出有意义的分析或者推导,它就不会显示推导的结果。
###### 编译器
假如我们想看到之前那段代码中x和y的类型,我们可以首先声明一个类模板但不定义。就像这样
```
template<typename T> //只对TD进行声明
class TD; //TD == "Type Displayer"
```
尝试实例化他们就会引发错误:
```
TD<decltype(x)> xType; //引出包含x和y
TD<decltype(y)> yType; //的类型的错误消息
```
###### 运行时输出
这种方法对一个对象如x或y调用typeid产生一个std::type_info的对象,然后std::type_info里面的成员函数name()来产生一个C风格的字符串(即一个const char*)表示变量的名字。调用std::type_info::name不保证返回任何有意义的东西,但是库的实现者尝试尽量使它们返回的结果有用。
```
std::cout << typeid(x).name() << '\n'; //显示x和y的类型
std::cout << typeid(y).name() << '\n';
```
*输出的类型信息中包含 “...” ,表示编译器忽略了 T 的部分类型 *
Boost TypeIndex库(通常写作Boost.TypeIndex)是更好的选择。这个库不是标准C++的一部分,也不是IDE或者TD这样的模板。Boost库(可在boost.com获得)是跨平台,开源,有良好的开源协议的库,这意味着使用Boost和STL一样具有高度可移植性。
```
#include <boost/type_index.hpp>
template<typename T>
void f(const T& param)
{
using std::cout;
using boost::typeindex::type_id_with_cvr;
//显示T
cout << "T = "
<< type_id_with_cvr<T>().pretty_name()
<< '\n';
//显示param类型
cout << "param = "
<< type_id_with_cvr<decltype(param)>().pretty_name()
<< '\n';
}
```
## 二 auto
#### Item 5 优先考虑auto而非显式类型声明
------------------------------------------------
C++ 14 支持的:
1. 如果使用C++14,将会变得更酷,因为lambda表达式中的形参也可以使用auto
###### 正如 Item2 和 Item6 讨论的,auto 类型的变量可能会踩到一些陷阱。
###### std::function是一个C++11标准模板库中的一个模板,它泛化了函数指针的概念。与函数指针只能指向函数不同,std::function可以指向任何可调用对象,也就是那些像函数一样能进行调用的东西。当你声明函数指针时你必须指定函数类型(即函数签名),同样当你创建std::function对象时你也需要提供函数签名,由于它是一个模板所以你需要在它的模板参数里面提供。
```
auto derefLess = //C++14版本
[](const auto& p1, //被任何像指针一样的东西
const auto& p2) //指向的值的比较函数
{ return *p1 < *p2; };
std::function<bool(const std::unique_ptr<Widget> &,
const std::unique_ptr<Widget> &)>
derefUPLess = [](const std::unique_ptr<Widget> &p1,
const std::unique_ptr<Widget> &p2)
{ return *p1 < *p2; };
```
上面对比 auto 和 std::function 的代码。语法冗长不说,还需要重复写很多形参类型,使用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我会尽我最大努力说服你使用lambda表达式代替std::bind)
###### 可怕的错误 1
```
std::unordered_map<std::string, int> m;
…
for(const std::pair<std::string, int>& p : m)
{
… //用p做一些事
}
```
看起来好像很合情合理的表达,但是这里有一个问题,你看到了吗?
要想看到错误你就得知道std::unordered_map的key是const的,所以hash table(std::unordered_map本质上的东西)中的std::pair的类型不是std::pair<std::string, int>,而是std::pair<const std::string, int>。但那不是在循环中的变量p声明的类型。编译器会努力的找到一种方法把std::pair<const std::string, int>(即hash table中的东西)转换为std::pair<std::string, int>(p的声明类型)。它会成功的,因为它会通过拷贝m中的对象创建一个临时对象,这个临时对象的类型是p想绑定到的对象的类型,即m中元素的类型,然后把p的引用绑定到这个临时对象上。在每个循环迭代结束时,临时对象将会销毁,如果你写了这样的一个循环,你可能会对它的一些行为感到非常惊讶,因为你确信你只是让成为p指向m中各个元素的引用而已。
使用auto可以避免这些很难被意识到的类型不匹配的错误:
```
for(const auto& p : m)
{
… //如之前一样
}
```
###### 首先,深呼吸,放松,auto是可选项,不是命令,在某些情况下如果你的专业判断告诉你使用显式类型声明比auto要更清晰更易维护,那你就不必再坚持使用auto。软件开发社区对于类型推导有丰富的经验,他们展示了在维护大型工业强度的代码上使用这种技术没有任何争议。
###### 如果初始化表达式的类型改变,则auto推导出的类型也会改变,这意味着使用auto可以帮助我们完成一些重构工作。但同时,也可能产生一些意料之外的变化。
#### Item 6 auto推导若非己愿,使用显式类型初始化惯用法
------------------------------------------------
###### std::vector<bool>::reference是一个代理类(proxy class)的例子:所谓代理类就是以模仿和增强一些类型的行为为目的而存在的类。很多情况下都会使用代理类,std::vector<bool>::reference展示了对std::vector<bool>使用operator[]来实现引用bit这样的行为。另外,C++标准模板库中的智能指针(见第4章)也是用代理类实现了对原始指针的资源管理行为。代理类的功能已被大家广泛接受。事实上,“Proxy”设计模式是软件设计这座万神庙中一直都存在的高级会员。一些代理类被设计于用以对客户可见。比如std::shared_ptr和std::unique_ptr。
其他的代理类则或多或少不可见,比如std::vector<bool>::reference就是不可见代理类的一个例子,还有它在std::bitset的胞弟std::bitset::reference。
```cpp
Matrix sum = m1 + m2 + m3 + m4;
```
可以使计算更加高效,只需要使让operator+返回一个代理类代理结果而不是返回结果本身。也就是说,对两个Matrix对象使用operator+将会返回如Sum<Matrix, Matrix>这样的代理类作为结果而不是直接返回一个Matrix对象。在std::vector<bool>::reference和bool中存在一个隐式转换,同样对于Matrix来说也可以存在一个隐式转换允许Matrix的代理类转换为Matrix,这让表达式等号“=”右边能产生代理对象来初始化sum。(这个对象应当编码整个初始化表达式,即类似于Sum<Sum<Sum<Matrix, Matrix>, Matrix>, Matrix>的东西。客户应该避免看到这个实际的类型。)
作为一个通则,不可见的代理类通常不适用于auto。这样类型的对象的生命期通常不会设计为能活过一条语句,所以创建那样的对象你基本上就走向了违反程序库设计基本假设的道路。std::vector<bool>::reference就是这种情况,我们看到违反这个基本假设将导致未定义行为。
实际上, 很多开发者都是在跟踪一些令人困惑的复杂问题或在单元测试出错进行调试时才看到代理类的使用。不管你怎么发现它们的,一旦看到auto推导了代理类的类型而不是被代理的类型,解决方案并不需要抛弃auto。auto本身没什么问题,问题是auto不会推导出你想要的类型。解决方案是强制使用一个不同的类型推导形式,这种方法我通常称之为显式类型初始器惯用法(the explicitly typed initialized idiom)。
对于Matrix来说,显式类型初始器惯用法是这样的:
```cpp
auto sum = static_cast<Matrix>(m1 + m2 + m3 + m4);
```
总结来说:不可见的代理类可能会使auto从表达式中推导出“错误的”类型,这个需要自己去注意。显式类型初始器惯用法强制auto推导出你想要的结果。但对于程序设计者,应该尽量不要让不可见的代理类活过一条语句,不要让他出现在 “程序员日常使用的雷达” 上方。
## 三 移步现代 C++
#### Item07:区别使用()和{}创建对象
------------------------------------------------
###### 1. 几种需要注意的创建对象的情况
```
Widget w1; //调用默认构造函数
Widget w2 = w1; //不是赋值运算,调用拷贝构造函数
w1 = w2; //是赋值运算,调用拷贝赋值运算符(copy operator=)
```
###### 2. 等号= 和 括号()不适用的情况
```
/* 括号初始化也能被用于为非静态数据成员指定默认初始值。C++11允许"="初始化不加花括号也拥有这种能力 */
class Widget{
…
private:
int x{ 0 }; //没问题,x初始值为0
int y = 0; //也可以
int z(0); //错误!
}
/* 不可拷贝的对象(例如std::atomic——见Item40)可以使用花括号初始化或者小括号初始化,但是不能使用"="初始化 */
std::atomic<int> ai1{ 0 }; //没问题
std::atomic<int> ai2(0); //没问题
std::atomic<int> ai3 = 0; //错误!
```
###### 3. 你越喜欢用auto,你就越不能用括号初始化。
###### 4. std::vector有一个非std::initializer_list构造函数允许你去指定容器的初始大小,以及使用一个值填满你的容器。但它也有一个std::initializer_list构造函数允许你使用花括号里面的值初始化容器。如果你创建一个数值类型的std::vector(比如std::vector<int>),然后你传递两个实参,把这两个实参放到小括号和放到花括号中大不相同:
```
std::vector<int> v1(10, 20); //使用非std::initializer_list构造函数
//创建一个包含10个元素的std::vector,
//所有的元素的值都是20
std::vector<int> v2{10, 20}; //使用std::initializer_list构造函数
//创建包含两个元素的std::vector,
//元素的值为10和20
```
###### 5. 空的花括号意味着没有实参,不是一个空的std::initializer_list。如果你想用空std::initializer来调用std::initializer_list构造函数,你就得创建一个空花括号作为函数实参——通过把空花括号放在小括号或者另一花括号内来界定你想传递的东西。
```
Widget w4({}); //使用空花括号列表调用std::initializer_list构造函数
Widget w5{{}}; //同上
```
###### 6. 请记住
- 括号初始化是最广泛使用的初始化语法,它防止变窄转换,并且对于C++最令人头疼的解析有天生的免疫性
- 在构造函数重载决议中,括号初始化尽最大可能与std::initializer_list参数匹配,即便其他构造函数看起来是更好的选择
- 对于数值类型的std::vector来说使用花括号初始化和小括号初始化会造成巨大的不同
- 在模板类选择使用小括号初始化或使用花括号初始化创建对象是一个挑战。
#### Item08:优先考虑别名声明而非typedefs
------------------------------------------------
###### 1. 优先考虑nullptr而非0和NULL
###### 2. 避免重载指针和整型,因为你不知道别人会不会把 NULL 当作指针来调用重载函数。有可能 NULL 会优先被决议为整形。
#### Item09:优先考虑别名声明而非typedefs
------------------------------------------------
###### 1. 不过有一个地方使用别名声明吸引人的理由是存在的:模板。特别地,别名声明可以被模板化(这种情况下称为别名模板alias templates)但是typedef不能。这使得C++11程序员可以很直接的表达一些C++98中只能把typedef嵌套进模板化的struct才能表达的东西。
```
template<typename T>
using MyAllocList = std::list<T, MyAlloc<T>>;
MyAllocList<Widget> lw;
```
对比
```
template<typename T>
struct MyAllocList {
typedef std::list<T, MyAlloc<T>> type;
};
MyAllocList<Widget>::type lw;
```
###### 2. 如果你想使用在一个模板内使用typedef声明一个链表对象,而这个对象又使用了模板形参,你就不得不在typedef前面加上typename。意思是:C++ 必须要在依赖类型名前加上typename(使用了模板类型?)如果使用别名声明定义一个MyAllocList,就不需要使用typename。
```
template<typename T>
class Widget { //Widget<T>含有一个
private: //MyAllocLIst<T>对象
typename MyAllocList<T>::type list; //作为数据成员
…
};
```
对比
```
template<typename T>
using MyAllocList = std::list<T, MyAlloc<T>>; //同之前一样
template<typename T>
class Widget {
private:
MyAllocList<T> list; //没有“typename”
… //没有“::type”
};
```
###### 3. C++11在type traits(类型特性)中给了你一系列工具去实现类型转换,如果要使用这些模板请包含头文件<type_traits>。里面有许许多多type traits,也不全是类型转换的工具,也包含一些可预测接口的工具。给一个你想施加转换的类型T,结果类型就是std::transformation<T>::type,比如:
```
std::remove_const<T>::type //从const T中产出T
std::remove_reference<T>::type //从T&和T&&中产出T
std::add_lvalue_reference<T>::type //从T中产出T&
```
###### 4. C++14 提供了C++11所有type traits转换的别名声明版本
#### Item10 优先考虑限域enum而非未限域enum
------------------------------------------------
###### 1. 使用限域 enum 有一下几个好处:
- 使用限域enum来减少命名空间污染,这是一个足够合理使用它而不是它的同胞未限域enum的理由
- 其实限域enum还有第二个吸引人的优点:在它的作用域中,枚举名是强类型。未限域enum中的枚举名会隐式转换为整型(现在,也可以转换为浮点类型)。
- 限域enum可以被前置声明,非限域枚举通过指定底层类型,也可以前置声明。
###### 2. 不能前置声明enum也是有缺点的。最大的缺点莫过于它可能增加编译依赖。比如说,修改修改一下枚举类型,所有包含该枚举类型的源文件都需要重新编译。这种enum很有可能用于整个系统,因此系统中每个包含这个头文件的组件都会依赖它。如果引入一个新状态值,那么可能整个系统都得重新编译,即使只有一个子系统——或者只有一个函数——使用了新添加的枚举名。
###### 3. 可以重写限域枚举的底层类型。非限域类型也可以通过下面的方式指定底层类型,从而进行前置声明。
``enum class Status: std::uint32_t; ``
###### 4. 限域enum避免命名空间污染而且不接受荒谬的隐式类型转换,但它并非万事皆宜,你可能会很惊讶听到至少有一种情况下非限域enum是很有用的。那就是牵扯到C++11的std::tuple的时候。访问 tuple 与其记住索引,不如直接用 enum 代替索引访问。
###### 5. 介绍了一下如何创建一个函数,返回枚举对应的底层类型的值。并给出了在 C++14 中的最佳实践。
#### Item11 优先考虑使用deleted函数而非使用未定义的私有声明
------------------------------------------------
###### 1 通常,deleted函数被声明为public而不是private。这也是有原因的。当客户端代码试图调用成员函数,C++会在检查deleted状态前检查它的访问性。当客户端代码调用一个私有的deleted函数,一些编译器只会给出该函数是private的错误(译注:而没有诸如该函数被deleted修饰的错误),即使函数的访问性不影响它是否能被使用。
###### 2 deleted函数还有一个重要的优势是任何函数都可以标记为deleted,而只有成员函数可被标记为private
```
bool isLucky(int number); //原始版本
bool isLucky(char) = delete; //拒绝char
bool isLucky(bool) = delete; //拒绝bool
bool isLucky(double) = delete; //拒绝float和double
```
###### 3 另一个deleted函数用武之地(private成员函数做不到的地方)是禁止一些模板的实例化。
```
template<>void processPointer<void>(void*) = delete;
template<>void processPointer<char>(char*) = delete;
```
###### 4 不能给特化的成员模函数指定一个不同于主函数模板的访问级别,因此用private无法阻制特化,但是delete并不需要一个不同的访问级别,可以在同一个命名空间甚至类外delete.
###### 5 类的模板函数特化如果是写在类外,不需要再进行类内声名
###### 6 函数模板不允许偏持化,只有类模板才能够偏特化7 模板为什么必须定义在头文件首先,如果模板没有使用是不会生成实例化的出数符号的.其次,如果模板定义放在了某个源文件之中,该源文件如果没有使用者就不会生成相应实例的函数及符号然后,包会了模板函数声名的源文件,使用函数模板实例,生成了调用符号,但是在链接的时侯却找不到定义。除非,在模板定义的源文件里加上特化的声名,让它生成符号。但这样,每种实何都要在源件声名一次实在太麻烦了。
参考链接:https://blog.csdn.net/imred/article/details/80261632
#### Item12 使用override声明重写函数
------------------------------------------------
###### 1. 如果基类的虚函数有引用限定符,派生类的重写就必须具有相同的引用限定符。如果没有,那么新声明的函数还是属于派生类,但是不会重写父类的任何函数。
###### 2. C++11引入了两个上下文关键字(contextual keywords),override和final(向虚函数添加final可以防止派生类重写。final也能用于类,这时这个类不能用作基类)。这两个关键字的特点是它们是保留的,它们只是位于特定上下文才被视为关键字。对于override,它只在成员函数声明结尾处才被视为关键字。
###### 3. 成员函数引用限定让我们可以区别对待左值对象和右值对象(即*this)
###### X.1 从左值初始化的,会调用拷贝构造而得到新对象; 从右值初始化的,会调用移动构造函数而得到新对象;
Widget w1; // 调用默认构造函数
Widget w2 = w1; // 不是赋值运算,调用拷贝构造函数
w1 = w2; // 是赋值运算,调用拷贝赋值运算符(copy operator=)
#### Item13 优先考虑const_iterator而非iterator
------------------------------------------------
###### 1. 优先考虑const_iterator 而非 iterator
###### 2. 在最大程度通用的代码中,优先考虑非成员函数版本的begin,end,rbegin等,而非同名成员函数
#### Item14 如果函数不抛出异常请使用noexcept
------------------------------------------------
##### 这一节读的其实有点云里雾里的。
###### noexcept是函数接口的一部分,这意味着调用者可能会依赖它noexcept
###### 函数较之于non-noexcept函数更容易优化
###### noexcept对于移动语义,swap,内存释放函数和析构函数非常有用
###### 大多数函数是异常中立的(译注:可能抛也可能不抛异常)而不是noexcept
#### Item15 尽可能的使用constexpr
------------------------------------------------
###### 1. 涉及到constexpr函数时,constexpr对象的使用情况就更有趣了。如果实参是编译期常量,这些函数将产出编译期常量;如果实参是运行时才能知道的值,它们就将产出运行时值。
###### 2. C++11中,constexpr函数的代码不超过一行语句:一个return。在 C++14 中,constexpr函数限制为只能获取和返回字面值类型,这基本上意味着那些有了值的类型能在编译期决定。在C++11中,除了void外的所有内置类型,以及一些用户定义类型都可以是字面值类型,因为构造函数和其他成员函数可能是constexpr。C++14 连 void 这一限制也已经放开了。C++11 的另一个限制是,constexpr 的成员函数是隐式的 const , 所以类似赋值器的函数是不能声明为 constexpr 的。C++14 也已经放开了这一限制。
###### 3. constexpr对象是const,它被在编译期可知的值初始化
###### 4. constexpr是对象和函数接口的一部分
#### Item16 让const成员函数线程安全
------------------------------------------------
###### 1. mutable 不能修饰 const 和 static 变量。它的本意和 const 相反,意味着变量可改变。另外,static 为类变量,由于 mutable 一般用于给对象的常量函数开绿灯,即:允许常量函数修改被 mutable 修饰的类对象成员,而又不能修改其他成员。因此,对于本身属于类的静态变量,mutable 似乎作用不大。
###### 2. std::atomic,std::mutex是一种只可移动类型(move-only type,一种可以移动但不能复制的类型)
###### 3. 使用std::atomic变量可能比互斥量提供更好的性能,但是它只适合操作单个变量或内存位置。
#### Item17 理解特殊成员函数的生成
------------------------------------------------
###### 1. 特殊成员函数是指C++自己生成的函数。C++98有四个:默认构造函数,析构函数,拷贝构造函数,拷贝赋值运算符。C++11特殊成员函数俱乐部迎来了两位新会员:移动构造函数和移动赋值运算符。
###### 2. 对于移动构造函数和移动赋值运算符,简单记住如果支持移动就会逐成员移动类成员和基类成员,如果不支持移动就执行拷贝操作就好了。
###### 3. Rule of Three_规则。这个规则告诉我们如果你声明了拷贝构造函数,拷贝赋值运算符,或者析构函数三者之一,你应该也声明其余两个。
###### 4. C++11对于特殊成员函数处理的规则如下:
- 默认构造函数:和C++98规则相同。仅当类不存在用户声明的构造函数时才自动生成。
- 析构函数:基本上和C++98相同;稍微不同的是现在析构默认noexcept(参见Item14)。和C++98一样,仅当基类析构为虚函数时该类析构才为虚函数。
- 拷贝构造函数:和C++98运行时行为一样:逐成员拷贝non-static数据。仅当类没有用户定义的拷贝构造时才生成。如果类声明了移动操作它就是delete的。当用户声明了拷贝赋值或者析构,该函数自动生成已被废弃。
- 拷贝赋值运算符:和C++98运行时行为一样:逐成员拷贝赋值non-static数据。仅当类没有用户定义的拷贝赋值时才生成。如果类声明了移动操作它就是delete的。当用户声明了拷贝构造或者析构,该函数自动生成已被废弃。
- 移动构造函数和移动赋值运算符:都对非static数据执行逐成员移动。仅当类没有用户定义的拷贝操作,移动操作或析构时才自动生成。
###### 5. 移动操作仅当类没有显式声明移动操作,拷贝操作,析构函数时才自动生成;成员函数模板不抑制特殊成员函数的生成。
## 四 智能指针
在C++11中存在四种智能指针:std::auto_ptr,std::unique_ptr,std::shared_ptr, std::weak_ptr
#### Item18 对于独占资源使用std::unique_ptr
------------------------------------------------
###### std::unique_ptr 是轻量级、快速的、只可移动(move-only)的管理专有所有权语义资源的智能指针
###### 如果一个异常传播到线程的基本函数(比如程序初始线程的main函数)外,或者违反noexcept说明(见Item14),局部变量可能不会被销毁;如果std::abort或者退出函数(如std::_Exit,std::exit,或std::quick_exit)被调用,局部变量一定没被销毁。)
###### 当使用自定义删除器时,删除器类型必须作为第二个类型实参传给std::unique_ptr。
###### 当使用默认删除器时(如delete),你可以合理假设std::unique_ptr 对象和原始指针大小相同。当自定义删除器时,情况可能不再如此。函数指针形式的删除器,通常会使std::unique_ptr的从一个字(word)大小增加到两个。对于函数对象形式的删除器来说,变化的大小取决于函数对象中存储的状态多少,无状态函数(stateless function)对象(比如不捕获变量的lambda表达式)对大小没有影响,这意味当自定义删除器可以实现为函数或者lambda时,尽量使用lambda。
###### std::unique_ptr 作为实现Pimpl Idiom(译注:pointer to implementation,一种隐藏实际实现而减弱编译依赖性的设计思想,《Effective C++》条款31对此有过叙述)的一种机制,它更为流行。
###### std::unique_ptr<T[]>有用的唯一情况是你使用类似C的API返回一个指向堆数组的原始指针,而你想接管这个数组的所有权。
###### std::unique_ptr是C++11中表示专有所有权的方法,但是其最吸引人的功能之一是它可以轻松高效的转换为std::shared_ptr:
###### 这就是std::unique_ptr非常适合用作工厂函数返回类型的原因的关键部分。 工厂函数无法知道调用者是否要对它们返回的对象使用专有所有权语义,或者共享所有权(即std::shared_ptr)是否更合适。 通过返回std::unique_ptr,工厂为调用者提供了最有效的智能指针,但它们并不妨碍调用者用其更灵活的兄弟替换它。
```
std::shared_ptr<Investment> sp = //将std::unique_ptr
makeInvestment(arguments); //转为std::shared_ptr
```
###### 默认情况,资源销毁通过delete实现,但是支持自定义删除器。有状态的删除器和函数指针会增加std::unique_ptr对象的大小
###### 将 std::unique_ptr 转化为 std::shared_ptr 非常简单
#### Item19 对于共享资源使用std::shared_ptr
------------------------------------------------
###### 使用std::make_shared创建std::shared_ptr可以避免引用计数的动态分配,但是还存在一些std::make_shared不能使用的场景,这时候引用计数就会动态分配。
###### 从另一个std::shared_ptr移动构造新std::shared_ptr会将原来的std::shared_ptr设置为null,那意味着老的std::shared_ptr不再指向资源,同时新的std::shared_ptr指向资源。这样的结果就是不需要修改引用计数值。因此移动std::shared_ptr会比拷贝它要快:拷贝要求递增引用计数值,移动不需要。
###### std::shared_ptr使用delete作为资源的默认销毁机制,但是它也支持自定义的删除器。这种支持有别于std::unique_ptr。对于std::unique_ptr来说,删除器类型是智能指针类型的一部分。对于std::shared_ptr则不是
###### 考虑有两个std::shared_ptr<Widget>,每个自带不同的删除器。但是因为pw1和pw2有相同的类型,所以它们都可以放到存放那个类型的对象的容器中。
```cpp
auto loggingDel = [](Widget *pw) //自定义删除器
{ //(和条款18一样)
makeLogEntry(pw);
delete pw;
};
std::unique_ptr< //删除器类型是
Widget, decltype(loggingDel) //指针类型的一部分
> upw(new Widget, loggingDel);
std::shared_ptr<Widget> //删除器类型不是
spw(new Widget, loggingDel); //指针类型的一部分
auto customDeleter1 = [](Widget *pw) { … }; //自定义删除器,
auto customDeleter2 = [](Widget *pw) { … }; //每种类型不同
std::shared_ptr<Widget> pw1(new Widget, customDeleter1);
std::shared_ptr<Widget> pw2(new Widget, customDeleter2);
std::vector<std::shared_ptr<Widget>> vpw{ pw1, pw2 };
```
###### 我前面提到了std::shared_ptr对象包含了所指对象的引用计数的指针。没错,但是有点误导人。因为引用计数是另一个更大的数据结构的一部分,那个数据结构通常叫做控制块(control block)。每个std::shared_ptr管理的对象都有个相应的控制块。控制块除了包含引用计数值外还有一个自定义删除器的拷贝,当然前提是存在自定义删除器。如果用户还指定了自定义分配器,控制块也会包含一个分配器的拷贝。
![](/i/ll/?i=f8c78d4314b74e46807ee8c77fa483ed.png#pic_center)
###### 这些规则造成的后果就是从原始指针上构造超过一个std::shared_ptr就会让你走上未定义行为的快车道,因为指向的对象有多个控制块关联。多个控制块意味着多个引用计数值,多个引用计数值意味着对象将会被销毁多次(每个引用计数一次)。那意味着像下面的代码是有问题的,很有问题,问题很大:
```cpp
auto pw = new Widget; //pw是原始指针
std::shared_ptr<Widget> spw1(pw, loggingDel); //为*pw创建控制块
std::shared_ptr<Widget> spw2(pw, loggingDel); //为*pw创建第二个控制块
```
###### std::shared_ptr给我们上了两堂课。第一,避免传给std::shared_ptr构造函数原始指针。通常替代方案是使用std::make_shared(参见Item21),不过上面例子中,我们使用了自定义删除器,用std::make_shared就没办法做到。第二,如果你必须传给std::shared_ptr构造函数原始指针,直接传new出来的结果,不要传指针变量。
###### std::unique_ptr 开销很小,转成 std::shared_ptr 很容易。但这种转换是单向的,std::shared_ptr 不能再转回 std::unique_ptr
###### std::shared_ptr不能处理的另一个东西是数组。和std::unique_ptr不同的是,std::shared_ptr的API设计之初就是针对单个对象的,没有办法std::shared_ptr<T[]>。一方面,std::shared_ptr没有提供operator[],所以数组索引操作需要借助怪异的指针算术。另一方面,std::shared_ptr支持转换为指向基类的指针,这对于单个对象来说有效,但是当用于数组类型时相当于在类型系统上开洞。(出于这个原因,std::unique_ptr<T[]> API禁止这种转换。)更重要的是,C++11已经提供了很多内置数组的候选方案(比如std::array,std::vector,std::string)。声明一个指向傻瓜数组的智能指针(译注:也是”聪明的指针“之意)几乎总是表示着糟糕的设计。
###### 原则:避免从原始指针变量上创建std::shared_ptr。
###### 影响性能的因素:动态分配控制块,任意大小的删除器和分配器,虚函数机制,原子性的引用计数修改。虚函数实在销毁的时候调用一次。
#### Item20 当 std::shared_ptr 可能悬空时使用 std::weak_ptr
------------------------------------------------
###### 1. 一个真正的智能指针应该跟踪所指对象,在悬空时知晓,悬空(dangle)就是指针指向的对象不再存在。这就是对std::weak_ptr最精确的描述。std::weak_ptr不能解引用,也不能测试是否为空值。因为std::weak_ptr不是一个独立的智能指针。它是std::shared_ptr的增强。简而言之,它就是一个辅助 std::shared_ptr 的指针,用来判断其是否悬空。
###### 2. 从 weak_ptr 得到 shared_ptr
```
std::shared_ptr<Widget> spw1 = wpw.lock(); //如果wpw过期,spw1就为空
auto spw2 = wpw.lock(); //同上,但是使用auto
// 另一种形式是以std::weak_ptr为实参构造std::shared_ptr。这种情况中,如果std::weak_ptr过期,会抛出一个异常:
std::shared_ptr<Widget> spw3(wpw); //如果wpw过期,抛出std::bad_weak_ptr异常
```
###### 3. std::weak_ptr 的用途主要有三类
3.1 在工厂函数里,如果对创建了的 std::shared_ptr 对象进行缓存,那么需要监督这些对象是否已经悬空,对于悬空的对象要从缓存里清理掉。缓存就可以采用 std::weak_ptr,监督悬空。
3.2 观察者设计模式中,此模式的主要组件是subjects(状态可能会更改的对象)和observers(状态发生更改时要通知的对象)。在大多数实现中,每个subject都包含一个数据成员,该成员持有指向其observers的指针。这使subjects很容易发布状态更改通知。subjects对控制observers的生命周期(即它们什么时候被销毁)没有兴趣,但是subjects对确保另一件事具有极大的兴趣,那事就是一个observer被销毁时,不再尝试访问它。一个合理的设计是每个subject持有一个std::weak_ptrs容器指向observers,因此可以在使用前检查是否已经悬空。
3.3 A B 两个对象,互相持有对方的 std::shared_ptr 时,会导致无法回收。当没有任何其他对象可以访问 A 或者 B 的时候,它们所持有的 std::shared_ptr 计数仍然为 1,无法自动销毁。所以,std::weak_ptr 可以用来解除引用。
###### 4. 总结
4.1 用 std::weak_ptr 替代可能会悬空的 std::shared_ptr。
4.2 std::weak_ptr 的潜在使用场景包括:缓存、观察者列表、打破std::shared_ptr环状结构。
#### Item21 优先考虑使用std::make_unique和std::make_shared而非new
------------------------------------------------
###### 共有三个 make 类函数:
1. std::make_unique (C++14)
2. std::make_shard (C++11)
3. std::allocate_shared,它行为和std::make_shared一样,只不过第一个参数是用来动态分配内存的allocator对象。
###### 为什么用 make 类函数更好
1. new 的方式需要写两次对象的类型
```
auto upw1(std::make_unique<Widget>()); //使用make函数
std::unique_ptr<Widget> upw2(new Widget); //不使用make函数auto spw1(std::make_shared<Widget>()); //使用make函数
std::shared_ptr<Widget> spw2(new Widget); //不使用make函数
```
2. 在编写异常安全代码时,使用std::make_unique而不是new与使用std::make_shared(而不是new)同样重要。在参数列表中,执行顺序是不确定的。那么,考虑如果 new Widget 已经执行了,接下来执行 computePriority() 函数的时候,产生了异常的情况。由于 std::shared_ptr<Widget>()还没有构造,所以 new Widget 出来的对象无人管理和释放,最终就会泄露。但如果使用 std::make_shared<Widget>() 就不会有问题。
本质原因还是,第一种方式是两步操作,而 make 的方式是一步操作。
```
processWidget(std::shared_ptr<Widget>(new Widget), //潜在的资源泄漏!
computePriority());
processWidget(std::make_shared<Widget>(), //没有潜在的资源泄漏
computePriority());
```
3. make_shared 的方式内存有优化,效率更高。它会一次性分配好所要管理的对象内存和 shared 要用的引用计数相关的内存块。它们在保存在一块内存,因此会同时申请和释放。这也带来了一些内存不能及时释放的问题。
###### 不适合用 make 的情况
本条款的建议是,更倾向于使用make函数,而不是完全依赖于它们。这是因为有些情况下它们不能或不应该被使用。
1. make函数都不允许指定自定义删除器(见Item18和19),但是std::unique_ptr和std::shared_ptr有构造函数这么做。
2. 希望用花括号初始化。make 不能接受花括号初始化,而 new 可以。但 item 30 提供了额外的方法,就是创建一个 std::initializer_list 对象,然后作为 make 接口小括号的内容传进去。
下面是针对 shared_ptr 的:
3. 使用make函数去创建重载了operator new和operator delete类的对象是个典型的糟糕想法。make 函数内部自定义的 allocator 和 删除器 可能无法支持重载的 new 和 delete。因为std::allocate_shared需要的内存总大小不等于动态分配的对象大小,还需要再加上控制块大小。
4. 直到最后一个std::shared_ptr和最后一个指向它的std::weak_ptr已被销毁,才会释放。如果对象类型非常大,而且销毁最后一个std::shared_ptr和销毁最后一个std::weak_ptr之间的时间很长,那么在销毁对象和释放它所占用的内存之间可能会出现延迟。std::weak_ptr 需要查询控制块,而控制块由于和对象内存在一起导致不能在 shared_ptr 指向的计数为0时就释放,是这个问题的根本原因。但与直接使用new相比,std::make_shared在大小和速度上的优势源于std::shared_ptr的控制块与指向的对象放在同一块内存中。因此,如果特别在意对象内存占用的时间和释放时机,则要慎用 std::make_shared
#### Item22 当使用Pimpl惯用法,请在实现文件中定义特殊成员函数
一个已经被声明,却还未被实现的类型,被称为未完成类型(incomplete type)。 Widget::Impl就是这种类型。 你能对一个未完成类型做的事很少,但是声明一个指向它的指针是可以的。这可以加速编译,并且意味着,如果这些头文件中有所变动,Widge t的**使用者**不会受到影响。
1. Pimpl 惯用法通过减少在类实现和类使用者之间的编译依赖来减少编译时间。
2. 对于std::unique_ptr类型的pImpl指针,需要在头文件的类里声明特殊的成员函数,但是在实现文件里面来实现他们。在实现文件里,即使是编译器自动生成的代码也可以工作。本质上的原因是,在自动生成的特殊成员函数里,需要知道 std::unique_ptr 所指向对象的具体类型。而放在头文件里的前置声明,不能提供确切类型。
3. 以上的建议只适用于std::unique_ptr,不适用于std::shared_ptr。
## 五 右值引用,移动语义,完美转发
当你第一次了解到移动语义(move semantics)和完美转发(perfect forwarding)的时候,它们看起来非常直观:
- 移动语义使编译器有可能用廉价的移动操作来代替昂贵的拷贝操作。正如拷贝构造函数和拷贝赋值操作符给了你控制拷贝语义的权力,移动构造函数和移动赋值操作符也给了你控制移动语义的权力。移动语义也允许创建只可移动(move-only)的类型,例如std::unique_ptr,std::future和std::thread。
- 完美转发使接收任意数量实参的函数模板成为可能,它可以将实参转发到其他的函数,使目标函数接收到的实参与被传递给转发函数的实参保持一致。
右值引用是连接这两个截然不同的概念的胶合剂。它是使移动语义和完美转发变得可能的基础语言机制。
**在本章的这些小节中,非常重要的一点是要牢记形参永远是左值,即使它的类型是一个右值引用。**
#### 自己总结几点:
###### 1 int && a = 1; 这里面 a 虽然是一个右值引用,但它本身是一个变量也就是一个左值。当用它去调用函数时,会作为左值来匹配。右值引用只是用来关联一个右值,仅此而已。在编译器看来,它就是一个左值。
###### 2 std::forward 最好只用于模板推导。简而言之,std::forward<T>(a) 函数的精髓主要在于 T 的变化,其内部实现基本就是这个:
```cpp
static_cast<_Tp&&>(__t);
```
对于通用引用(以 int 为例),_Tp 有可能被推导为 int, int&, int&& 等类型。a 在 forward 内部会被去除引用,并转成 int && 类型,这个时候,_Tp 和 a 的类型就要进行一次折叠,折叠规则为:
- X& &、X& &&、X&& &都折叠成X&
- X&& &&折叠为X&&
显然,只有 _ 为 int && 或者是 int 的时候,a 会被转成右值。而 _Tp 为 int& 的时候,a 会被转成左值。而 _Tp 转成 int & 正是通用引用被传递左值时,推导的结果。因此,会保留左值的属性。
###### 3 函数定义
```cpp
// std::move() 定义
template<typename T>
typename remove_reference<T>::type && move(T&& t)
{
return static_cast<typename remove_reference<T>::type &&>(t);
}
```
```cpp
// std::forward() 定义
template<typename _Tp>
constexpr _Tp&&
forward(typename std::remove_reference<_Tp>::type& __t) noexcept
{ return static_cast<_Tp&&>(__t); }
template<typename _Tp>
constexpr _Tp&&
forward(typename std::remove_reference<_Tp>::type&& __t) noexcept
{
static_assert(!std::is_lvalue_reference<_Tp>::value, "template argument"
" substituting _Tp is an lvalue reference type");
return static_cast<_Tp&&>(__t);
}
```
###### 4 比较有用的参考资料
// 提出了一个好问题,虽然解释的有些笼统
https://zhuanlan.zhihu.com/p/92486757
// 解释的比较清楚
https://www.cnblogs.com/shadow-lr/p/Introduce_Std-move.html
###### 5 移动语义函数虽然不会销毁被移动对象,但一般会使被移动对象内部的数据不可用。因此,尽量还是不要再用了吧,但还是要释放。
#### Item 23 理解std::move和std::forward
------------------------------------------------
1 为了了解std::move和std::forward,一种有用的方式是从它们不做什么这个角度来了解它们。std::move不移动(move)任何东西,std::forward也不转发(forward)任何东西。在运行时,它们不做任何事情。它们不产生任何可执行代码,一字节也没有。std::move和std::forward仅仅是执行转换(cast)的函数(事实上是函数模板)。std::move无条件的将它的实参转换为右值,而std::forward只在特定情况满足时下进行转换。
2 从一个对象中移动出某个值通常代表着修改该对象,所以语言不允许const对象被传递给可以修改他们的函数(例如移动构造函数)。从这个例子中,可以总结出两点。第一,不要在你希望能移动对象的时候,声明他们为const。对const对象的移动请求会悄无声息的被转化为拷贝操作。第二点,std::move不仅不移动任何东西,而且它也不保证它执行转换的对象可以被移动。关于std::move,你能确保的唯一一件事就是将它应用到一个对象上,你能够得到一个右值。
3 与std::move总是无条件的将它的实参为右值不同,std::forward只有在满足一定条件的情况下才执行转换。std::forward是有条件的转换。要明白什么时候它执行转换,什么时候不,想想std::forward的典型用法。
4 区分:真正的右值 和 绑定了右值的引用。
5 文末总结
std::move执行到右值的无条件的转换,但就自身而言,它不移动任何东西。
std::forward只有当它的参数被绑定到一个右值时,才将参数转换为右值。
std::move和std::forward在运行期什么也不做。
#### Item 24 区分通用引用与右值引用
------------------------------------------------
1 如果类型声明的形式不是标准的type&&,或者如果类型推导没有发生,那么type&&代表一个右值引用。如果你看见“T&&”不带有类型推导,那么你看到的就是一个右值引用。
2 如果一个函数模板形参的类型为T&&,并且T需要被推导得知,或者如果一个对象被声明为auto&&,这个形参或者对象就是一个通用引用。
3 因为通用引用是引用,所以它们必须被初始化。一个通用引用的初始值决定了它是代表了右值引用还是左值引用。如果初始值是一个右值,那么通用引用就会是对应的右值引用,如果初始值是一个左值,那么通用引用就会是一个左值引用。对那些是函数形参的通用引用来说,初始值在调用函数的时候被提供。
4 需要格外注意辨别是否为通用引用的情况:
4.1 通用引用声明的形式必须正确,并且该形式是被限制的。它必须恰好为“T&&”。下面的形式不属于通用引用:
```
template <typename T>
void f(std::vector<T>&& param); //param是一个右值引用
```
4.2 如果你看见“T&&”不带有类型推导,那么你看到的就是一个右值引用
```cpp
void f(Widget&& param); //没有类型推导,
//param是一个右值引用
Widget&& var1 = Widget(); //没有类型推导,
//var1是一个右值引用
```
4.3 即使一个简单的const修饰符的出现,也足以使一个引用失去成为通用引用的资格
```cpp
template <typename T>
void f(const T&& param); //param是一个右值引用
```
4.4 如果你在一个模板里面看见了一个函数形参类型为“T&&”,你也许觉得你可以假定它是一个通用引用。错!这是由于在模板内部并不保证一定会发生类型推导。
```cpp
template<class T, class Allocator = allocator<T>> //来自C++标准
class vector
{
public:
void push_back(T&& x);
…
}
```
#### Item 25 对右值引用使用std::move,对通用引用使用std::forward
------------------------------------------------
###### 总而言之,当把右值引用转发给其他函数时,右值引用应该被无条件转换为右值(通过std::move),因为它们总是绑定到右值;当转发通用引用时,通用引用应该有条件地转换为右值(通过std::forward),因为它们只是有时绑定到右值。
- 局部变量绑定到右值引用,传递到函数里是比较危险的。比如说 string,它有可能被移动,然后值就莫名其妙的被改变了。因为右值引用,被默认为是可以被移动的。
- 在某些情况,你可能需要在一个函数中多次使用绑定到右值引用或者通用引用的对象,并且确保在完成其他操作前,这个对象不会被移动。这时,你只想在最后一次使用时,使用std::move(对右值引用)或者std::forward(对通用引用)。在此之前,它一定是个左值。(因为函数形参都是引用变量)
- RVO
- "如果对要被拷贝到返回值的右值引用形参使用std::move,会把拷贝构造变为移动构造,”他们想,“我也可以对我要返回的局部对象应用同样的优化。”这是错的。原因就是,返回值优化(return value optimization,RVO)。这种优化,实际上已经把将要返回的局部变量,在分配给函数返回值的内存中构造了。
- 编译器可能会在按值返回的函数中消除对局部对象的拷贝(或者移动),如果满足(1)局部对象与函数返回值的类型相同;(2)局部对象就是要返回的东西。(适合的局部对象包括大多数局部变量(比如makeWidget里的w),还有作为return语句的一部分而创建的临时对象。函数形参不满足要求。
- 在某些情况下,将std::move应用于局部变量可能是一件合理的事(即,你把一个变量传给函数,并且知道不会再用这个变量),但是满足RVO的return语句或者返回一个传值形参并不在此列。
请记住(这里加粗的部分还是很值得我们注意的)
- **最后一次使用时**,在右值引用上使用std::move,在通用引用上使用std::forward。
- 对**按值返回**的函数要返回的右值引用和通用引用,执行上一条相同的操作。
- 如果局部对象可以被返回值优化消除,就绝不使用std::move或者std::forward。
#### Item 26 避免在通用引用上重载
------------------------------------------------
###### 使用通用引用的函数在C++中是最贪婪的函数。它们几乎可以精确匹配任何类型的实参(极少不适用的实参在Item30中介绍)。这也是把重载和通用引用组合在一块是糟糕主意的原因:通用引用的实现会匹配比开发者预期要多得多的实参类型。
- 有两个重载的 logAndAdd。使用通用引用的那个推导出T的类型是short,因此可以精确匹配。对于int类型参数的重载也可以在short类型提升后匹配成功。根据正常的重载解决规则,精确匹配优先于类型提升的匹配,所以被调用的是通用引用的重载。
- 重载规则规定当模板实例化函数和非模板函数(或者称为“正常”函数)匹配优先级相当时,优先使用“正常”函数。拷贝构造函数(正常函数)因此胜过具有相同签名的模板实例化函数。
- 完美转发构造函数是糟糕的实现,因为对于non-const左值,它们比拷贝构造函数而更匹配,而且会劫持派生类对于基类的拷贝和移动构造函数的调用。
#### Item 27 熟悉通用引用重载的替代方法
------------------------------------------------
针对于 Item26 中的情况,本文列出了几种方法:
1. 放弃重载,即改用不同的函数名字
2. 放弃通用引用。将传递通用引用替换为传递lvalue-refrence-to-const (const T&)
3. 传值。该设计遵循Item41中给出的建议,即在你知道要拷贝时就按值传递,因此会参考那个条款来详细讨论如何设计与工作,效率如何。
4. tag dispatch 是在重载时,接受通用引用参数,但是重载规则不仅依赖通用引用形参,还依赖新引入的标签形参,标签值设计来保证有不超过一个的重载是合适的匹配。结果是标签来决定采用哪个重载函数。通用引用参数可以生成精确匹配的事实在这里并不重要。类型 std::true_type 和 std::false_type 是“标签”(tag),其唯一目的就是强制重载解析按照我们的想法来执行。注意到我们甚至没有对这些参数进行命名。他们在运行时毫无用处,事实上我们希望编译器可以意识到这些标签形参没被使用,然后在程序执行时优化掉它们。
5. 约束使用通用引用的模板 (this chapter is a little bit complex)
约束模板使用的技术点:
- SFINAE ("Substitution Failure Is Not An Error")
- std::enable_if
- std::is_base_of
- std::is_integral
```cpp
class Person {
public:
template<
typename T,
typename = std::enable_if_t<
!std::is_base_of<Person, std::decay_t<T>>::value
&&
!std::is_integral<std::remove_reference_t<T>>::value
>
>
explicit Person(T&& n) //对于std::strings和可转化为
: name(std::forward<T>(n)) //std::strings的实参的构造函数
{ … }
explicit Person(int idx) //对于整型实参的构造函数
: name(nameFromIdx(idx))
{ … }
… //拷贝、移动构造函数等
private:
std::string name;
};
```
6. 折中。上述使用的完美转发方法,会导致编译错误出现在实际出错位置之前,因此消息可读性差。尽管可以如本方法中,加一些检查,但检查出现的时机也不是太好。。
- static_assert
- std::is_constructible
- "perfect forwarding" in this chapter will cause another problem. The error message is hard to understand. Even though, we could use std::is_constructible to check compatibility, the static_assert 在构造函数体中,但是转发的代码作为成员初始化列表的部分在检查之前。
###### 总结
- 通用引用和重载的组合替代方案包括使用不同的函数名,通过lvalue-reference-to-const传递形参,按值传递形参,使用tag dispatch。
- 通过std::enable_if约束模板,允许组合通用引用和重载使用,但它也控制了编译器在哪种条件下才使用通用引用重载。
- 通用引用参数通常具有高效率的优势,但是可用性就值得斟酌。
#### Item 28 理解引用折叠
------------------------------------------------
###### 引用折叠(reference collapsing)
是的,禁止你声明引用的引用,但是编译器会在特定的上下文中产生这些,模板实例化就是其中一种情况。当编译器生成引用的引用时,引用折叠指导下一步发生什么。
- 如果任一引用为左值引用,则结果为左值引用。否则(即,如果引用都是右值引用),结果为右值引用。
- 因为fParam是通用引用,我们知道类型参数T的类型根据f被传入实参(即用来实例化fParam的表达式)是左值还是右值来编码。std::forward的作用是当且仅当传给f的实参为右值时,即T为非引用类型,才将fParam(左值)转化为一个右值。
###### 引用折叠发生在四种情况下:
1. 也是最常见的就是模板实例化。
2. 是auto变量的类型生成,具体细节类似于模板,因为auto变量的类型推导基本与模板类型推导雷同(参见Item2)。
3. 第三种情况是typedef和别名声明的产生和使用中
4. 在分析decltype期间,出现了引用的引用,引用折叠规则就会起作用
###### Summary
1. 引用折叠发生在四种情况下:模板实例化,auto类型推导,typedef与别名声明的创建和使用,decltype。
2. 当编译器在引用折叠环境中生成了引用的引用时,结果就是单个引用。有左值引用折叠结果就是左值引用,否则就是右值引用。
3. 通用引用就是在特定上下文的右值引用,上下文是通过类型推导区分左值还是右值,并且发生引用折叠的那些地方。
#### Item 29 假定移动操作不存在,成本高,未被使用
------------------------------------------------
std::array,这是C++11中的新容器。std::array本质上是具有STL接口的内置数组。这与其他标准容器将内容存储在堆内存不同。假定Widget类的移动操作比复制操作快,移动Widget的std::array就比复制要快。所以std::array确实支持移动操作。但是使用std::array的移动操作还是复制操作都将花费线性时间的开销,因为每个容器中的元素终归需要拷贝或移动一次,这与“移动一个容器就像操作几个指针一样方便”的含义相去甚远。
存储具体数据在堆内存的容器,本身只保存了指向堆内存中容器内容的指针(真正实现当然更复杂一些,但是基本逻辑就是这样)。这个指针的存在使得在常数时间移动整个容器成为可能,只需要从源容器拷贝保存指向容器内容的指针到目标容器,然后将源指针置为空指针就可以了
std::string提供了常数时间的移动操作和线性时间的复制操作。这听起来移动比复制快多了,但是可能不一定。许多字符串的实现采用了小字符串优化(small string optimization,SSO)。“小”字符串(比如长度小于15个字符的)存储在了std::string的缓冲区中,并没有存储在堆内存,移动这种存储的字符串并不必复制操作更快。
###### 因此,存在几种情况,C++11的移动语义并无优势:
1. 没有移动操作:要移动的对象没有提供移动操作,所以移动的写法也会变成复制操作。
2. 移动不会更快:要移动的对象提供的移动操作并不比复制速度更快。
3. 移动不可用:进行移动的上下文要求移动操作不会抛出异常,但是该操作没有被声明为noexcept。
4. 源对象是左值:除了极少数的情况外(例如Item25),只有右值可以作为移动操作的来源。
#### Item 30 熟悉完美转发失败的情况
------------------------------------------------
###### What's Perfect Forwarding
在我们开始误差探索之前,有必要回顾一下“完美转发”的含义。“转发”仅表示将一个函数的形参传递——就是转发——给另一个函数。对于第二个函数(被传递的那个)目标是收到与第一个函数(执行传递的那个)完全相同的对象。这规则排除了按值传递的形参,因为它们是原始调用者传入内容的拷贝。我们希望被转发的函数能够使用最开始传进来的那些对象。指针形参也被排除在外,因为我们不想强迫调用者传入指针。关于通常目的的转发,我们将处理引用形参。
完美转发(perfect forwarding)意味着我们不仅转发对象,我们还转发显著的特征:它们的类型,是左值还是右值,是const还是volatile。结合到我们会处理引用形参,这意味着我们将使用通用引用(参见Item24),因为通用引用形参被传入实参时才确定是左值还是右值。
###### 完美转发失效的情况
1. 花括号初始化器
问题:将花括号初始化传递给未声明为std::initializer_list的函数模板形参,被判定为——就像标准说的——“非推导上下文”。会报错。但使用花括号初始化的auto的变量的类型推导是成功的。
解决:用 auto 变量推导出 {} 初始化器类型,然后再将 auto 临时变量传到函数里。
2. 0或者NULL作为空指针
问题:0或者NULL作为空指针给模板时,类型推导会出错,会把传来的实参推导为一个整型类型(典型情况为int)而不是指针类型。
解决:使用 nullptr
3. 仅有声明的整型static const数据成员
问题:通常,编译器会对此类成员实行常量传播,因此无需定义。但是,在使用完美转发时,作为传递给模板的参数,会绑定到引用上,引用本质上是一个可以自动解引用的指针,因此需要绑定到一个地址。所以,需要定义。
解决:给这类成员一个定义。注意,定义中不要重复初始化,只在声明时初始化一次即可。
注意:确实,根据标准,通过引用传递 MinVals 要求有定义。但不是所有的实现都强制要求这一点。所以,取决于你的编译器和链接器。为了具有可移植性,只要给整型static const提供一个定义。dsf
4. 重载函数的名称和模板名称
问题:单用重载函数processVal是没有类型信息的,所以就不能类型推导,完美转发失败。如果我们试图使用函数模板而不是(或者也加上)重载函数的名字,同样的问题也会发生。一个函数模板不代表单独一个函数,它表示一个函数族
解决:引导选择正确版本的processVal或者产生正确的workOnVal实例
5. 位域
问题:函数实参使用位域这种类型
解决:自己创建副本然后利用副本调用完美转发。
###### Summary
- 当模板类型推导失败或者推导出错误类型,完美转发会失败。
- 导致完美转发失败的实参种类有花括号初始化,作为空指针的0或者NULL,仅有声明的整型static const数据成员,模板和重载函数的名字,位域。
## 第6章 lambda表达式
与 lambda 相关的词汇可能会令人疑惑,这里做一下简单的回顾:
- lambda表达式(lambda expression)就是一个表达式。下面是部分源代码。在代码中的高亮部分就是lambda。
```cpp
std::find_if(container.begin(), container.end(),
[](int val){ return 0 < val && val < 10; }); //译者注:本行高亮
```
- 闭包(enclosure)是lambda创建的运行时对象。依赖捕获模式,闭包持有被捕获数据的副本或者引用。在上面的std::find_if调用中,闭包是作为第三个实参在运行时传递给std::find_if的对象。
- 闭包类(closure class)是从中实例化闭包的类。每个lambda都会使编译器生成唯一的闭包类。lambda中的语句成为其闭包类的成员函数中的可执行指令。
非正式的讲,模糊lambda,闭包和闭包类之间的界限是可以接受的。但是,在随后的Item中,区分什么存在于编译期(lambdas 和闭包类),什么存在于运行时(闭包)以及它们之间的相互关系是重要的。
#### Item 31 避免使用默认捕获模式
------------------------------------------------
按引用捕获会导致闭包中包含了对某个局部变量或者形参的引用,变量或形参只在定义lambda的作用域中可用。如果该lambda创建的闭包生命周期超过了局部变量或者形参的生命周期,那么闭包中的引用将会变成悬空引用。
按引用捕获会导致闭包中包含了对某个局部变量或者形参的引用,变量或形参只在定义lambda的作用域中可用。如果该lambda创建的闭包生命周期超过了局部变量或者形参的生命周期,那么闭包中的引用将会变成悬空引用。比起“[&]”传达的意思,显式捕获能让人更容易想起“确保没有悬空变量”。
但在通常情况下,按值捕获并不能完全解决悬空引用的问题。这里的问题是如果你按值捕获的是一个指针,你将该指针拷贝到lambda对应的闭包里,但这样并不能避免lambda外delete这个指针的行为,从而导致你的副本指针变成悬空指针。
**捕获只能应用于lambda被创建时所在作用域里的non-static局部变量(包括形参)** 这个特定的问题可以通过给你想捕获的数据成员做一个局部副本,然后捕获这个副本去解决.
**注意**:每一个non-static成员函数都有一个this指针,每次你使用一个类内的数据成员时都会使用到这个指针。例如,在任何Widget成员函数中,编译器会在内部将divisor替换成this->divisor。So when you think the variables of object has been value captured by lambda, actually it's not. If "this" pointer become invalid, the value is invalid too.
**what're the differences between access members of object and global variables( include static variables) ?**
1. lambda can access global and static variables just because they can be "see" by it.
2. lambda can't access members of object by default capture. ( like: [] ), but can access by [=]. That's because pointer "this" is captured.
###### Summary
- 默认的按引用捕获可能会导致悬空引用。
- 默认的按值捕获对于悬空指针很敏感(尤其是this指针),并且它会误导人产生lambda是独立的想法。
#### Item 32 使用初始化捕获来移动对象到闭包中
------------------------------------------------
reference of unique_ptr: https://blog.csdn.net/qq_33266987/article/details/78784286
在某些场景下,按值捕获和按引用捕获都不是你所想要的。如果你有一个只能被移动的对象(例如std::unique_ptr或std::future)要进入到闭包里,使用C++11是无法实现的。如果你要复制的对象复制开销非常高,但移动的成本却不高(例如标准库中的大多数容器),并且你希望的是宁愿移动该对象到闭包而不是复制它。然而C++11却无法实现这一目标。
C++14 中初始化捕获, 左侧的作用域是闭包类,右侧的作用域和lambda定义所在的作用域相同。在上面的示例中,“=”左侧的名称pw表示闭包类中的数据成员,而右侧的名称pw表示在lambda上方声明的对象,即由调用std::make_unique去初始化的变量。因此,“pw = std::move(pw)”的意思是“在闭包中创建一个数据成员pw,并使用将std::move应用于局部变量pw的结果来初始化该数据成员”。
**lambda表达式只是生成一个类和创建该类型对象的一种简单方式而已。没什么事是你用lambda可以做而不能自己手动实现的。**
默认情况下,从lambda生成的闭包类中的operator()成员函数为const的。这具有在lambda主体内把闭包中的所有数据成员渲染为const的效果。 如果将lambda声明为mutable,则闭包类中的operator()将不会声明为const,并且在lambda的形参声明中省略const也是合适的
###### 使用std::bind模仿移动捕获:
- 无法移动构造一个对象到C++11闭包,但是可以将对象移动构造进C++11的bind对象。
- 在C++11中模拟移动捕获包括将对象移动构造进bind对象,然后通过传引用将移动构造的对象传递给lambda。
- 由于bind对象的生命周期与闭包对象的生命周期相同,因此可以将bind对象中的对象视为闭包中的对象。
###### Summary:
- 使用C++14的初始化捕获将对象移动到闭包中。
- 在C++11中,通过手写类或std::bind的方式来模拟初始化捕获。
#### Item33 对auto&&形参使用decltype以std::forward它们 (C++14)
------------------------------------------------
泛型lambda(generic lambdas)是C++14中最值得期待的特性之一——因为在lambda的形参中可以使用auto关键字。这个特性的实现是非常直截了当的:即在闭包类中的operator()函数是一个函数模版。
**表明用右值引用类型和用非引用类型去初始化std::forward产生的相同的结果** (引用折叠)
lambda的完美转发可以写成:
```cpp
auto f =
[](auto&& param)
{
return
func(normalize(std::forward<decltype(param)>(param)));
};
```
再加上6个点,就可以让我们的lambda完美转发接受多个形参了,因为C++14中的lambda也可以是可变形参的:
```cpp
auto f =
[](auto&&... params)
{
return
func(normalize(std::forward<decltype(params)>(params)...));
};
```
###### Summary
- 对auto&&形参使用decltype以std::forward它们。
#### Item 34 考虑lambda而非std::bind
------------------------------------------------
在C++11中,lambda几乎总是比std::bind更好的选择。 从C++14开始,lambda的作用不仅强大,而且是完全值得使用的。
1. 优先lambda而不是std::bind的最重要原因是lambda更易读。
对于门外汉来说,占位符“_1”完全是一个魔法,但即使是知情的读者也必须从思维上将占位符中的数字映射到其在std::bind形参列表中的位置,以便明白调用setSoundB时的第一个实参会被传递进setAlarm,作为调用setAlarm的第二个实参。在对std::bind的调用中未标识此实参的类型,因此读者必须查阅setAlarm声明以确定将哪种实参传递给setSoundB。
2. Different parameters estimate time for expression. 在lambda中,表达式steady_clock::now() + 1h显然是setAlarm的实参。调用setAlarm时将对其进行计算。可以理解:我们希望在调用setAlarm后一小时响铃。但是,在std::bind调用中,将steady_clock::now() + 1h作为实参传递给了std::bind,而不是setAlarm。在调用std::bind时对表达式进行求值,并且该表达式产生的时间将存储在产生的bind对象中。结果,警报器将被设置为在调用std::bind后一小时发出声音,而不是在调用setAlarm一小时后发出。
3. 重载函数, 编译器无法确定应将两个setAlarm函数中的哪一个传递给std::bind。 它们仅有的是一个函数名称,而这个单一个函数名称是有歧义的。编译器不太可能通过函数指针内联函数,这意味着与通过setSoundL进行调用相比,通过setSoundB对setAlarm的调用,其函数不大可能被内联.因此,使用lambda可能会比使用std::bind能生成更快的代码。
4. 与lambda相比,使用std::bind进行编码的代码可读性较低,表达能力较低,并且效率可能较低。 在C++14中,没有std::bind的合理用例。 但是,在C++11中,可以在两个受约束的情况下证明使用std::bind是合理的:
- 移动捕获。C++11的lambda不提供移动捕获,但是可以通过结合lambda和std::bind来模拟。 有关详细信息,请参阅Item32,该条款还解释了在C++14中,lambda对初始化捕获的支持消除了这个模拟的需求。
- 多态函数对象。因为bind对象上的函数调用运算符使用完美转发,所以它可以接受任何类型的实参(以Item30中描述的完美转发的限制为界限)。当你要绑定带有模板化函数调用运算符的对象时,此功能很有用。 例如这个类,
###### Summary
- 与使用std::bind相比,lambda更易读,更具表达力并且可能更高效。
- 只有在C++11中,std::bind可能对实现移动捕获或绑定带有模板化函数调用运算符的对象时会很有用 (C++14 auto for lambda is better)。
## 第7章 并发API
C++11的伟大成功之一是将并发整合到语言和库中。C++对于并发的大量支持是在对编译器作者约束的层面。由此产生的语言保证意味着在C++的历史中,开发者首次通过标准库可以写出跨平台的多线程程序。
#### Item 35 优先考虑基于任务的编程而非基于线程的编程
------------------------------------------------
###### All content in this item worth read again! The are all important.
我们假设调用doAsyncWork的代码对于其提供的返回值是有需求的。基于线程的方法对此无能为力,而基于任务的方法就简单了,因为std::async返回的future提供了get函数(从而可以获取返回值)。如果doAsycnWork发生了异常,get函数就显得更为重要,因为get函数可以提供抛出异常的访问,而基于线程的方法,如果doAsyncWork抛出了异常,程序会直接终止(通过调用std::terminate)。
如果你当前的并发编程采用基于任务的方式,在这些技术发展中你会持续获得回报。相反如果你直接使用std::thread编程,处理线程耗尽、资源超额、负责均衡问题的责任就压在了你身上,更不用说你对这些问题的解决方法与同机器上其他程序采用的解决方案配合得好不好了。
###### Summary
- std::thread API不能直接访问异步执行的结果,如果执行函数有异常抛出,代码会终止执行。
- 基于线程的编程方式需要手动的线程耗尽、资源超额、负责均衡、平台适配性管理。
- 通过带有默认启动策略的std::async进行基于任务的编程方式会解决大部分问题。
#### Item 36 如果有异步的必要请指定std::launch::async
------------------------------------------------
**线程本地存储(thread-local storage,TLS)**
1. 有两种标准策略,每种都通过std::launch这个限域enum的一个枚举名表示
- std::launch::async启动策略意味着f必须异步执行,即在不同的线程。
- std::launch::deferred启动策略意味着f仅当在std::async返回的future上调用get或者wait时才执行。这表示f推迟到存在这样的调用时才执行(译者注:异步与并发是两个不同概念,这里侧重于惰性求值)。当get或wait被调用,f会同步执行,即调用方被阻塞,直到f运行结束。如果get和wait都没有被调用,f将不会被执行。
- 可能让人惊奇的是,std::async的默认启动策略——你不显式指定一个策略时它使用的那个——不是上面中任意一个。相反,是求或在一起的。因此默认策略允许f异步或者同步执行。如同Item35中指出,这种灵活性允许std::async和标准库的线程管理组件承担线程创建和销毁的责任,避免资源超额,以及平衡负载。
2. 机器资源超额或者线程耗尽的条件,此时任务推迟执行才最有可能发生. In that case, It could be never schedule before wait or get.
3. 只要满足以下条件,std::async的默认启动策略就可以使用:
- 任务不需要和执行get或wait的线程并行执行。
- 读写哪个线程的thread_local变量没什么问题。
- 可以保证会在std::async返回的future上调用get或wait,或者该任务可能永远不会执行也可以接受。
- 使用wait_for或wait_until编码时考虑到了延迟状态。
###### Summary
- std::async的默认启动策略是异步和同步执行兼有的。
- 这个灵活性导致访问thread_locals的不确定性,隐含了任务可能不会被执行的意思,会影响调用基于超时的wait的程序逻辑。
- 如果异步执行任务非常关键,则指定std::launch::async。
#### Item 37 使 std::thread 在所有路径最后都 unjoinable
------------------------------------------------
1. 每个std::thread对象处于两个状态之一:可结合的(joinable)或者不可结合的(unjoinable)。
- 可结合状态的std::thread对应于正在运行或者可能要运行的异步执行线程。比如,对应于一个阻塞的(blocked)或者等待调度的线程的std::thread是可结合的,对应于运行结束的线程的std::thread也可以认为是可结合的。
- 不可结合的std::thread对象包括:
- 默认构造的std::threads。这种std::thread没有函数执行,因此没有对应到底层执行线程上。
- 已经被移动走的std::thread对象。移动的结果就是一个std::thread原来对应的执行线程现在对应于另一个std::thread。
- 已经被join的std::thread 。在join之后,std::thread不再对应于已经运行完了的执行线程。
- 已经被detach的std::thread 。detach断开了std::thread对象与执行线程之间的连接。
2. 标准委员会认为,销毁可结合的线程如此可怕以至于实际上禁止了它(规定销毁可结合的线程导致程序终止)。
- 隐式 join. NO!
- 隐式 detach. NO!
**些对象称为RAII对象(RAII objects),从RAII类中实例化。(RAII全称为 “Resource Acquisition Is Initialization”(资源获得即初始化),尽管技术关键点在析构上而不是实例化上)。**
3. std::thread should be last in initialized list. 在这个类中,这个顺序没什么特别之处,但是通常,可能一个数据成员的初始化依赖于另一个,因为std::thread对象可能会在初始化结束后就立即执行函数了,所以在最后声明是一个好习惯。这样就能保证一旦构造结束,在前面的所有数据成员都初始化完毕,可以供std::thread数据成员绑定的异步运行的线程安全使用。
4. 在不可结合的std::thread上调用join或detach会导致未定义行为。
5. 通常,仅当所有都为const成员函数时,在一个对象同时调用多个成员函数才是安全的。
6. 使用ThreadRAII来保证在std::thread的析构时执行join有时不仅可能导致程序表现异常,还可能导致程序挂起。“适当”的解决方案是此类程序应该和异步执行的lambda通信,告诉它不需要执行了,可以直接返回,但是C++11中不支持可中断线程(interruptible threads)。可以自行实现,但是这不是本书讨论的主题。(关于这一点,Anthony Williams的《C++ Concurrency in Action》(Manning Publications,2012)的section 9.2中有详细讨论。)(译者注:此书中文版已出版,名为《C++并发编程实战》,且本文翻译时(2020)已有第二版出版。)
###### Summary
- 在所有路径上保证thread最终是不可结合的。
- 析构时join会导致难以调试的表现异常问题。
- 析构时detach会导致难以调试的未定义行为。
- 声明类数据成员时,最后声明std::thread对象。
#### Item 38 关注不同线程句柄的析构行为
------------------------------------------------
###### 1 可结合的std::thread析构会终止你的程序,因为两个其他的替代选择——隐式join或者隐式detach都是更加糟糕的。但是,future的析构表现有时就像执行了隐式join,有时又像是隐式执行了detach,有时又没有执行这两个选择。它永远不会造成程序终止。
###### 2 这些规则听起来好复杂。
我们真正要处理的是一个简单的“正常”行为以及一个单独的例外。正常行为是future析构函数销毁future。就是这样。那意味着不join也不detach,也不运行什么,只销毁future的数据成员(当然,还做了另一件事,就是递减了共享状态中的引用计数,这个共享状态是由引用它的future和被调用者的std::promise共同控制的。这个引用计数让库知道共享状态什么时候可以被销毁。对于引用计数的一般信息参见Item19。)
正常行为的例外情况仅在某个future同时满足下列所有情况下才会出现:
- 它关联到由于调用std::async而创建出的共享状态。
- 任务的启动策略是std::launch::async(参见Item36),原因是运行时系统选择了该策略,或者在对std::async的调用中指定了该策略。
- 这个future是关联共享状态的最后一个future。对于std::future,情况总是如此,对于std::shared_future,如果还有其他的std::shared_future,与要被销毁的future引用相同的共享状态,则要被销毁的future遵循正常行为(即简单地销毁它的数据成员)。
只有当上面的三个条件都满足时,future的析构函数才会表现“异常”行为,就是在异步任务执行完之前阻塞住。实际上,这相当于对由于运行std::async创建出任务的线程隐式join。
###### 3 我们知道future没有关联std::async创建的共享状态,所以析构函数肯定正常方式执行。
Because the Shard State is associated with std::packaged_task
```cpp
int calcValue(); //要运行的函数
std::packaged_task<int()> //包覆calcValue以异步运行
pt(calcValue);
auto fut = pt.get_future(); //从pt获取future
```
###### Summary
- future的正常析构行为就是销毁 future 本身的数据成员。
- 引用了共享状态——使用 std::async 启动的未延迟任务建立的那个——的最后一个future的析构函数会阻塞住,直到任务完成。
#### Item 39 对于一次性事件通信考虑使用void的futures
------------------------------------------------
有时,一个任务通知另一个异步执行的任务发生了特定的事件很有用,因为第二个任务要等到这个事件发生之后才能继续执行。事件也许是一个数据结构已经初始化,也许是计算阶段已经完成,或者检测到重要的传感器值。这种情况下,线程间通信的最佳方案是什么?
1. 一个明显的方案就是使用条件变量(condition variable,简称condvar)。
问题需要注意:
- 两个任务在程序逻辑下互不干扰,也就没有必要使用互斥锁。但是条件变量的方法必须使用互斥锁,这就留下了令人不适的设计。
- 如果在反应任务wait之前检测任务通知了条件变量,反应任务会挂起。
- wait语句虚假唤醒。正确的代码通过确认要等待的条件确实已经发生来处理这种情况,并将这个操作作为唤醒后的第一个操作。但是我们考虑的场景中,它正在等待的条件是检测线程负责识别的事件的发生情况。反应线程可能无法确定等待的事件是否已发生。这就是为什么它在等待一个条件变量!
2. 下一个诀窍是共享的布尔型flag。flag被初始化为false。当检测线程识别到发生的事件,将flag置位.
问题需要注意:
- 不好的一点是反应任务中轮询的开销。在任务等待flag被置位的时间里,任务基本被阻塞了,但是一直在运行。这样,反应线程占用了可能能给另一个任务使用的硬件线程,每次启动或者完成它的时间片都增加了上下文切换的开销,并且保持核心一直在运行状态,否则的话本来可以停下来省电。一个真正阻塞的任务不会发生上面的任何情况。这也是基于条件变量的优点,因为wait调用中的任务真的阻塞住了。
3. 将条件变量和flag的设计组合起来很常用。一个flag表示是否发生了感兴趣的事件,但是通过互斥锁同步了对该flag的访问。因为互斥锁阻止并发访问该flag,所以如Item40所述,不需要将flag设置为std::atomic。
```cpp
std::condition_variable cv; //跟之前一样
std::mutex m;
bool flag(false); //不是std::atomic
… //检测某个事件
{
std::lock_guard<std::mutex> g(m); //通过g的构造函数锁住m
flag = true; //通知反应任务(第1部分)
} //通过g的析构函数解锁m
cv.notify_one(); //通知反应任务(第2部分)
```
反应任务代码如下:
```cpp
… //准备作出反应
{ //跟之前一样
std::unique_lock<std::mutex> lk(m); //跟之前一样
cv.wait(lk, [] { return flag; }); //使用lambda来避免虚假唤醒
… //对事件作出反应(m被锁定)
}
… //继续反应动作(m现在解锁)
```
问题需要注意:
- 这种方案是可以工作的,但是不太优雅。
4. 检测任务有一个std::promise对象(即通信信道的写入端),反应任务有对应的future。当检测任务看到事件已经发生,设置std::promise对象(即写入到通信信道)。同时,wait会阻塞住反应任务直到std::promise被设置。
问题需要注意:
- std::promise和future之间有个共享状态,并且共享状态是动态分配的。因此你应该假定此设计会产生基于堆的分配和释放开销。
- 也许更重要的是,std::promise只能设置一次。std::promise和future之间的通信是一次性的:不能重复使用。这是与基于条件变量或者基于flag的设计的明显差异,条件变量和flag都可以通信多次。(条件变量可以被重复通知,flag也可以重复清除和设置。)
- 一次通信可能没有你想象中那么大的限制。假定你想创建一个挂起的系统线程。就是,你想避免多次线程创建的那种经常开销,以便想要使用这个线程执行程序时,避免潜在的线程创建工作。或者你想创建一个挂起的线程,以便在线程运行前对其进行设置这样的设置包括优先级或者核心亲和性(core affinity)。假设你仅仅想要对某线程挂起一次(在创建后,运行线程函数前),使用void的future就是一个可行方案。
```cpp
std::promise<void> p; //跟之前一样
void detect() //现在针对多个反映线程
{
auto sf = p.get_future().share(); //sf的类型是std::shared_future<void>
std::vector<std::thread> vt; //反应线程容器
for (int i = 0; i < threadsToRun; ++i) {
vt.emplace_back([sf]{ sf.wait(); //在sf的局部副本上wait;
react(); }); //emplace_back见条款42
}
… //如果这个“…”抛出异常,detect挂起!
p.set_value(); //所有线程解除挂起
…
for (auto& t : vt) { //使所有线程不可结合;
t.join(); //“auto&”见条款2
}
}
```
###### Summary
- 对于简单的事件通信,基于条件变量的设计需要一个多余的互斥锁,对检测和反应任务的相对进度有约束,并且需要反应任务来验证事件是否已发生。
- 基于flag的设计避免的上一条的问题,但是是基于轮询,而不是阻塞。
- 条件变量和flag可以组合使用,但是产生的通信机制很不自然。
- 使用std::promise和future的方案避开了这些问题,但是这个方法使用了堆内存存储共享状态,同时有只能使用一次通信的限制。
#### Item 40 对于并发使用std::atomic,对于特殊内存使用volatile
------------------------------------------------
1. 一旦std::atomic对象被构建,在其上的操作表现得像操作是在互斥锁保护的关键区内,但是通常这些操作是使用特定的机器指令实现,这比锁的实现更高效。
2. 要理解原子性的范围, 读-改-写(read-modify-write,RMW)操作,它们整体作为原子执行。这是std::atomic类型的最优的特性之一:一旦std::atomic对象被构建,所有成员函数,包括RMW操作,从其他线程来看都是原子性的。
>冗余访问(redundant loads)和无用存储(dead stores)
>顺序一致性(sequential consistency)
>弱(weak)(亦称松散的,relaxed)
>读-改-写(read-modify-write,RMW)
3. Why volatile is useful. Very Important !!! Very helpful to understand volatile!!!
可能你会想谁会写这种重复读写的代码(技术上称为冗余访问(redundant loads)和无用存储(dead stores)),答案是开发者不会直接写——至少我们不希望开发者这样写。但是在编译器拿到看起来合理的代码,执行了模板实例化,内联和一系列重排序优化之后,结果会出现冗余访问和无用存储,所以编译器需要摆脱这样的情况并不少见。
这种优化仅仅在内存表现正常时有效。“特殊”的内存不行。最常见的“特殊”内存是用来做内存映射I/O的内存。这种内存实际上是与外围设备(比如外部传感器或者显示器,打印机,网络端口)通信,而不是读写通常的内存(比如RAM)。这种情况下,再次考虑这看起来冗余的代码:
```cpp
auto y = x; //读x
y = x; //再次读x
```
如果x的值是一个温度传感器上报的,第二次对于x的读取就不是多余的,因为温度可能在第一次和第二次读取之间变化。
看起来冗余的写操作也类似。比如在这段代码中:
```cpp
x = 10; //写x
x = 20; //再次写x
```
如果x与无线电发射器的控制端口关联,则代码是给无线电发指令,10和20意味着不同的指令。优化掉第一条赋值会改变发送到无线电的指令流。
volatile是告诉编译器我们正在处理特殊内存。意味着告诉编译器“不要对这块内存执行任何优化”。所以如果x对应于特殊内存,应该声明为volatile:
```cpp
volatile int x;
```
考虑对我们的原始代码序列有何影响:
```cpp
auto y = x; //读x
y = x; //再次读x(不会被优化掉)
x = 10; //写x(不会被优化掉)
x = 20; //再次写x
```
如果x是内存映射的(或者已经映射到跨进程共享的内存位置等),这正是我们想要的。
突击测试!在最后一段代码中,y是什么类型:int还是volatile int?(y的类型使用auto类型推导,所以使用Item2中的规则。规则上说非引用非指针类型的声明(就是y的情况),const和volatile限定符被拿掉。y的类型因此仅仅是int。这意味着对y的冗余读取和写入可以被消除。在例子中,编译器必须执行对y的初始化和赋值两个语句,因为x是volatile的,所以第二次对x的读取可能会产生一个与第一次不同的值。)
4. std::atomic不适合这种场景。编译器被允许消除对std::atomic的冗余操作, 对于特殊内存,显然这是不可接受的。
5. std::atomic类型的拷贝操作是被删除的(参见Item11)。因为有个很好的理由删除。想象一下如果y使用x来初始化会发生什么。因为x是std::atomic类型,y的类型被推导为std::atomic(参见Item2)。我之前说了std::atomic最好的特性之一就是所有成员函数都是原子性的,但是为了使从x拷贝初始化y的过程是原子性的,编译器不得不生成代码,把读取x和写入y放在一个单独的原子性操作中。硬件通常无法做到这一点,因此std::atomic不支持拷贝构造。出于同样的原因,拷贝赋值也被删除了,这也是为什么从x赋值给y也编译失败。
```cpp
// 当x是std::atomic时,这两条语句都无法编译通过:
auto y = x; //错误
y = x; //错误
````
6. Very Interesting to say:
- std::atomic用在并发编程中,对访问特殊内存没用。
- volatile用于访问特殊内存,对并发编程没用。
7. std::atomic和volatile用于不同的目的,所以可以结合起来使用:
```cpp
volatile std::atomic<int> vai; //对vai的操作是原子性的,且不能被优化掉
```
如果vai变量关联了内存映射I/O的位置,被多个线程并发访问,这会很有用。
8. 一些开发者在即使不必要时也尤其喜欢使用std::atomic的load和store函数,因为这在代码中显式表明了这个变量不“正常”。强调这一事实并非没有道理。因为访问std::atomic确实会比non-std::atomic更慢一些,我们也看到了std::atomic会阻止编译器对代码执行一些特定的,本应被允许的顺序重排。调用load和store可以帮助识别潜在的可扩展性瓶颈。
> one thread write, one or multiple thread read:
>- first need to be atomic operation. If memory not align( cpu can't load data one time), could read strange data.
>- atomic assure sequence execution in program. but normal RMW can not according to this article.
###### Summary
- std::atomic用于在不使用互斥锁情况下,来使变量被多个线程访问的情况。是用来编写并发程序的一个工具。
- volatile用在读取和写入不应被优化掉的内存上。是用来处理特殊内存的一个工具。
#### Item 41 对于移动成本低且总是被拷贝的可拷贝形参,考虑按值传递
------------------------------------------------
##### This chapter need to read again!!!
> reference is an alias of variable. (under layer is a pointer) so use it just as same as original variable.
> r-value is a temporary and never-use-again value. so can be moved to new object.
当形参通过赋值进行拷贝时,分析按值传递的开销是复杂的。(Because it could need to destroy some exist memory which cause overhead) 通常,最有效的经验就是“在证明没问题之前假设有问题”,就是除非已证明按值传递会为你需要的形参类型产生可接受的执行效率,否则使用重载或者通用引用的实现方式。
在调用链中,每个函数都使用传值,因为“只多了一次移动的开销”,但是整个调用链总体就会产生无法忍受的开销,通过引用传递,调用链不会增加这种开销。
如果不熟悉**对象切片问题**,可以先通过搜索引擎了解一下。这样你就知道切片问题是C++98中默认按值传递名声不好的另一个原因(要在效率问题的原因之上)。有充分的理由来说明为什么你学习C++编程的第一件事就是避免用户自定义类型进行按值传递。
###### Summary
- 对于可拷贝,移动开销低,而且无条件被拷贝的形参,按值传递效率基本与按引用传递效率一致,而且易于实现,还生成更少的目标代码。
- **?????? 通过构造拷贝形参可能比通过赋值拷贝形参开销大的多。(没搞懂什么意思)**
- 按值传递会引起切片问题,所说不适合基类形参类型。
#### Item 42 考虑使用置入代替插入
------------------------------------------------
插入函数接受对象去插入,而置入函数接受对象的构造函数的实参去插入。这种差异允许置入函数避免插入函数所必需的临时对象的创建和销毁。
If all the following are true, emplacement will almost certainly out‐perform insertion:
- The value being added is constructed into the container, not assigned.
- The argument type(s) being passed differ from the type held by the container.
- The container is unlikely to reject the new value as a duplicate.
In the emplacement functions,
perfect-forwarding defers the creation of the resource-managing objects until they
can be constructed in the container’s memory, and that opens a window during
which exceptions can lead to resource leaks.
But back to push_back and emplace_back and, more generally, the insertion func‐
tions versus the emplacement functions. Emplacement functions use direct initializa‐
tion, which means they may use explicit constructors. Insertion functions employ
copy initialization.
The lesson to take away is that when you use an emplacement function, be especially
careful to make sure you’re passing the correct arguments, because even explicit
constructors will be considered by compilers as they try to find a way to interpret
your code as valid.
// C++ code style
https://google.github.io/styleguide/
###### Warning
- Emplacement functions may perform type conversions that would be rejected
by insertion functions.
标签:std,函数,Notes,Modern,C++,类型,引用,######,ptr From: https://www.cnblogs.com/beautiful-scenery/p/17488778.html