fix typo (#53)

fix typo
This commit is contained in:
Zhenhua Wang 2020-11-05 21:39:18 +08:00 committed by GitHub
parent 1970edd9e7
commit 564fe91e67
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
6 changed files with 89 additions and 87 deletions

View File

@ -15,13 +15,13 @@ public:
...
};
```
掌控它们生成和行为的规则类似于拷贝系列。移动操作仅在需要的时候生成如果生成了就会对非static数据执行逐成员的移动。那意味着移动构造函数根据rhs参数里面对应的成员移动构造出新部分移动赋值运算符根据参数里面对应的非static成员移动赋值。移动构造函数也移动构造基类部分如果有的话移动赋值运算符也是移动赋值基类部分。
掌控它们生成和行为的规则类似于拷贝系列。移动操作仅在需要的时候生成如果生成了就会对非static数据执行逐成员的移动。那意味着移动构造函数根据`rhs`参数里面对应的成员移动构造出新部分移动赋值运算符根据参数里面对应的非static成员移动赋值。移动构造函数也移动构造基类部分如果有的话移动赋值运算符也是移动赋值基类部分。
现在,当我对一个数据成员或者基类使用移动构造或者移动赋值,没有任何保证移动一定会真的发生。逐成员移动,实际上,更像是逐成员移动**请求**,因为对不可移动类型使用移动操作实际上执行的是拷贝操作。逐成员移动的核心是对对象使用**std::move**,然后函数决议时会选择执行移动还是拷贝操作。**Item 23**包括了这个操作的细节。本章中,简单记住如果支持移动就会逐成员移动类成员和基类成员,如果不支持移动就执行拷贝操作就好了。
现在,当我对一个数据成员或者基类使用移动构造或者移动赋值,没有任何保证移动一定会真的发生。逐成员移动,实际上,更像是逐成员移动**请求**,因为对不可移动类型使用移动操作实际上执行的是拷贝操作。逐成员移动的核心是对对象使用**std::move**,然后函数决议时会选择执行移动还是拷贝操作。**Item 23**包括了这个操作的细节。本章中,简单记住如果支持移动就会逐成员移动类成员和基类成员,如果不支持移动就执行拷贝操作就好了。
两个拷贝操作是独立的声明一个不会限制编译器声明另一个。所以如果你声明一个拷贝构造函数但是没有声明拷贝赋值运算符如果写的代码用到了拷贝赋值编译器会帮助你生成拷贝赋值运算符重载。同样的如果你声明拷贝赋值运算符但是没有拷贝构造代码用到拷贝构造编译器就会生成它。上述规则在C++98和C++11中都成立。
如果你声明了某个移动函数,编译器就不再生成另一个移动函数。这与复制函数的生成规则不太一样:两个复制函数是独立的,声明一个不会影响另一个的默认生成。这条规则的背后原因是,如果你声明了某个移动函数,就表明这个类型的移动操作不再是“逐一移动成员变量”的语义,即你不需要编译器默认生成的移动函数的语义,因此编译器也不会为你生成另一个移动函数
如果你声明了某个移动函数,编译器就不再生成另一个移动函数。这与复制函数的生成规则不太一样:两个复制函数是独立的,声明一个不会影响另一个的默认生成。这条规则的背后原因是,如果你声明了某个移动函数,就表明这个类型的移动操作不再是“逐一移动成员变量”的语义,即你不需要编译器默认生成的移动函数的语义,因此编译器也不会为你生成另一个移动函数
再进一步,如果一个类显式声明了拷贝操作,编译器就不会生成移动操作。这种限制的解释是如果声明拷贝操作就暗示着默认逐成员拷贝操作不适用于该类,编译器会明白如果默认拷贝不适用于该类,移动操作也可能是不适用的。
@ -95,7 +95,7 @@ private:
C++11对于特殊成员函数处理的规则如下
+ 默认构造函数和C++98规则相同。仅当类不存在用户声明的构造函数时才自动生成
+ 默认构造函数和C++98规则相同。仅当类不存在用户声明的构造函数时才自动生成
+ 析构函数基本上和C++98相同稍微不同的是现在析构默认**noexcept**参见Item14。和C++98一样仅当基类析构为虚函数时该类析构才为虚函数。
+ 拷贝构造函数和C++98运行时行为一样逐成员拷贝非static数据。仅当类没有用户定义的拷贝构造时才生成。如果类声明了移动操作它就是**delete**。当用户声明了拷贝赋值或者析构,该函数不再自动生成。
+ 拷贝赋值运算符和C++98运行时行为一样逐成员拷贝赋值非static数据。仅当类没有用户定义的拷贝赋值时才生成。如果类声明了移动操作它就是**delete**。当用户声明了拷贝构造或者析构,该函数不再自动生成。
@ -115,6 +115,6 @@ class Widget {
编译器仍会生成移动和拷贝操作假设正常生成它们的条件满足即使可以模板实例化产出拷贝构造和拷贝赋值运算符的函数签名。当T为Widget时。很可能你会决定这是一个不值得承认的边缘情况但是我提到它是有道理的Item16将会详细讨论它可能带来的后果。
记住:
+ 特殊成员函数是编译器可能自动生成的函数:默认构造,析构,拷贝操作,移动操作
+ 移动操作仅当类没有显式声明移动操作,拷贝操作,析构时才自动生成
+ 拷贝构造仅当类没有显式声明拷贝构造时才自动生成并且如果用户声明了移动操作拷贝构造就是delete。拷贝赋值运算符仅当类没有显式声明拷贝赋值运算符时才自动生成并且如果用户声明了移动操作拷贝赋值运算符就是delete。当用户声明了析构函数拷贝操作不再自动生成
+ 特殊成员函数是编译器可能自动生成的函数:默认构造,析构,拷贝操作,移动操作
+ 移动操作仅当类没有显式声明移动操作,拷贝操作,析构时才自动生成
+ 拷贝构造仅当类没有显式声明拷贝构造时才自动生成并且如果用户声明了移动操作拷贝构造就是delete。拷贝赋值运算符仅当类没有显式声明拷贝赋值运算符时才自动生成并且如果用户声明了移动操作拷贝赋值运算符就是delete。当用户声明了析构函数拷贝操作不再自动生成

View File

@ -2,18 +2,18 @@
诗人和歌曲作家喜欢爱。有时候喜欢计数。很少情况下两者兼有。受伊丽莎白·巴雷特·勃朗宁Elizabeth Barrett Browning对爱和数的不同看法的启发“我怎么爱你”让我数一数。”和保罗·西蒙Paul Simon“离开你的爱人必须有50种方法。”我们可以试着枚举一些为什么原始指针很难被爱的原因
1. 它的声明不能指示所指到底是单个对象还是数组
2. 它的声明没有告诉你用完后是否应该销毁它,即指针是否拥有所指之物
3. 如果你决定你应该销毁对象所指没人告诉你该用delete还是其他析构机制比如将指针传给专门的销毁函数
4. 如果你发现该用delete。 原因1说了不知道是delete单个对象还是delete数组。如果用错了结果是未定义的
5. 假设你确定了指针所指,知道销毁机制,也很难确定你在所有执行路径上都执行了销毁操作(包括异常产生后的路径)。少一条路径就会产生资源泄漏,销毁多次还会导致未定义行为
1. 它的声明不能指示所指到底是单个对象还是数组
2. 它的声明没有告诉你用完后是否应该销毁它,即指针是否拥有所指之物
3. 如果你决定你应该销毁对象所指没人告诉你该用delete还是其他析构机制比如将指针传给专门的销毁函数
4. 如果你发现该用delete。 原因1说了不知道是delete单个对象还是delete数组。如果用错了结果是未定义的
5. 假设你确定了指针所指,知道销毁机制,也很难确定你在所有执行路径上都执行了销毁操作(包括异常产生后的路径)。少一条路径就会产生资源泄漏,销毁多次还会导致未定义行为
6. 一般来说没有办法告诉你指针是否变成了悬空指针dangling pointers即内存中不再存在指针所指之物。悬空指针会在对象销毁后仍然指向它们。
原始指针是强大的工具,当然,另一方面几十年的经验证明,只要注意力稍有疏忽,这个强大的工具就会攻击它的主人。
智能指针是解决这些问题的一种办法。智能指针包裹原始指针,它们的行为看起来像被包裹的原始指针,但避免了原始指针的很多陷阱。你应该更倾向于智能指针而不是原始指针。几乎原始指针能做的所有事情智能指针都能做,而且出错的机会更少。
在C++11中存在四种智能指针`std::auto_ptr,std::unique_ptr,std::shared_ptr,std::weak_ptr`。都是被设计用来帮助管理动态对象的生命周期,在适当的时间通过适当的方式来销毁对象,以避免出现资源泄露或者异常行为。
在C++11中存在四种智能指针`std::auto_ptr`, `std::unique_ptr`, `std::shared_ptr`,` std::weak_ptr`。都是被设计用来帮助管理动态对象的生命周期,在适当的时间通过适当的方式来销毁对象,以避免出现资源泄露或者异常行为。
`std::auto_ptr`是C++98的遗留物它是一次标准化的尝试后来变成了C++11的`std::unique_ptr`。要正确的模拟原生制作需要移动语义但是C++98没有这个东西。取而代之`std::auto_ptr`拉拢拷贝操作来达到自己的移动意图。这导致了令人奇怪的代码(拷贝一个`std::auto_ptr`会将它本身设置为null和令人沮丧的使用限制比如不能将`std::auto_ptr`放入容器)。
@ -51,7 +51,7 @@ std::unique_ptr<Investment>
makeInvestment(Ts&&... params);
```
调用者应该在单独的作用域中使用返回的`std::unique_ptr`智能指针
调用者应该在单独的作用域中使用返回的`std::unique_ptr`智能指针
```cpp
{
@ -66,7 +66,7 @@ makeInvestment(Ts&&... params);
默认情况下销毁将通过delete进行但是在构造过程中可以自定义`std::unique_ptr`指向对象的析构函数任意函数或者函数对象包括lambda。如果通过`makeInvestment`创建的对象不能直接被删除,应该首先写一条日志,可以实现如下:
```cpp
auto delInvmt = [](Investemnt* pInvestment)
auto delInvmt = [](Investment* pInvestment)
{
makeLogEntry(pInvestment);
delete pInvestment;
@ -96,7 +96,7 @@ makeInvestment(Ts&& params)
这个实现确实相当棒,如果你理解了:
- `delInvmt`是自定义的从makeInvestmetn返回的析构函数。所有的自定义的析构行为接受要销毁对象的原始指针然后执行销毁操作。如上例子。使用lambda创建`delInvmt`是方便的,而且,正如稍后看到的,比编写常规的函数更有效
- `delInvmt`是自定义的从`makeInvestment`返回的析构函数。所有的自定义的析构行为接受要销毁对象的原始指针然后执行销毁操作。如上例子。使用lambda创建`delInvmt`是方便的,而且,正如稍后看到的,比编写常规的函数更有效
- 当使用自定义删除器时,必须将其作为第二个参数传给`std::unique_ptr`。对于decltype更多信息查看Item3
@ -104,9 +104,9 @@ makeInvestment(Ts&& params)
- 尝试将原始指针比如new创建赋值给`std::unique_ptr`通不过编译,因为不存在从原始指针到智能指针的隐式转换。这种隐式转换会出问题,所以禁止。这就是为什么通过`reset`来传递new指针的原因
- 使用new时要使用`std::forward`作为参数来完美转发给makeInvestment查看Item 25。这使调用者提供的所有信息可用于正在创建的对象的构造函数
- 使用new时要使用`std::forward`作为参数来完美转发给`makeInvestment`查看Item 25。这使调用者提供的所有信息可用于正在创建的对象的构造函数
- 自定义删除器的参数类型是Investment\*尽管真实的对象类型是在makeInvestment内部创建的它最终通过在lambda表达式中作为Investment\*对象被删除。这意味着我们通过基类指针删除派生类实例,为此,基类必须是虚函数析构
- 自定义删除器的参数类型是`Investment*`,尽管真实的对象类型是在`makeInvestment`内部创建的它最终通过在lambda表达式中作为`Investment*`对象被删除。这意味着我们通过基类指针删除派生类实例,为此,基类必须是虚函数析构
```cpp
class Investment {
@ -117,13 +117,13 @@ makeInvestment(Ts&& params)
};
```
在C++14中函数的返回类型推导存在参阅Item 3意味着makeInvestment可以更简单封装的方式实现
在C++14中函数的返回类型推导存在参阅Item 3意味着`makeInvestment`可以更简单,封装的方式实现:
```cpp
template<typename... Ts>
makeInvestment(Ts&& params)
{
auto delInvmt = [](Investemnt* pInvestment)
auto delInvmt = [](Investment* pInvestment)
{
makeLogEntry(pInvestment);
delete pInvestment;
@ -145,10 +145,10 @@ makeInvestment(Ts&& params)
}
```
我之前说过,当使用默认删除器时,你可以合理假设`std::unique_ptr`和原始指针大小相同。当自定义删除器时,情况可能不再如此。删除器是个函数指针,通常会使`std::unique_ptr`的字节从一个增加到两个。对于删除器的函数对象来说大小取决于函数对象中存储的状态多少无状态函数对象比如没有捕获的lambda表达式大没有影响这意味当自定义删除器可以被lambda实现时尽量使用lambda
我之前说过,当使用默认删除器时,你可以合理假设`std::unique_ptr`和原始指针大小相同。当自定义删除器时,情况可能不再如此。删除器是个函数指针,通常会使`std::unique_ptr`的字节从一个增加到两个。对于删除器的函数对象来说大小取决于函数对象中存储的状态多少无状态函数对象比如没有捕获的lambda表达式对大没有影响这意味当自定义删除器可以被lambda实现时尽量使用lambda
```cpp
auto delInvmt = [](Investemnt* pInvestment)
auto delInvmt = [](Investment* pInvestment)
{
makeLogEntry(pInvestment);
delete pInvestment;
@ -167,21 +167,21 @@ std::unique_ptr<Investment, void(*)(Investment*)>
makeInvestment(Ts&&... params); //返回Investment*的指针加至少一个函数指针的大小
```
具有很多状态的自定义删除器会产生大尺寸`std::unique_ptr`对象。如果你发现自定义删除器使得你的`std::unique_ptr`变得过大,你需要审视修改你的设计
具有很多状态的自定义删除器会产生大尺寸`std::unique_ptr`对象。如果你发现自定义删除器使得你的`std::unique_ptr`变得过大,你需要审视修改你的设计
工厂函数不是`std::unique_ptr`的唯一常见用法。作为实现**Pimpl Idiom**的一种机制它更为流行。代码并不复杂但是在某些情况下并不直观所以这安排在Item22的专门主题中
工厂函数不是`std::unique_ptr`的唯一常见用法。作为实现**Pimpl Idiom**的一种机制它更为流行。代码并不复杂但是在某些情况下并不直观所以这安排在Item22的专门主题中
`std::unique_ptr`有两种形式,一种用于单个对象(`std::unique_ptr<T>`),一种用于数组(`std::unique_ptr<T[]>`)。结果就是,指向哪种形式没有歧义。`std::unique_ptr`的API设计自动匹配你的用法比如[]操作符就是数组对象,\*和->就是单个对象专有
`std::unique_ptr`有两种形式,一种用于单个对象(`std::unique_ptr<T>`),一种用于数组(`std::unique_ptr<T[]>`)。结果就是,指向哪种形式没有歧义。`std::unique_ptr`的API设计自动匹配你的用法,比如[]操作符就是数组对象,\*和->就是单个对象专有
数组的`std::unique_ptr`的存在应该不被使用,因为`std::array,std::vector,std::string`这些更好用的数据容器应该取代原始数组。原始数组的使用唯一情况是使用C的API时
数组的`std::unique_ptr`的存在应该不被使用,因为`std::array`, `std::vector`, `std::string`这些更好用的数据容器应该取代原始数组。原始数组的使用唯一情况是你使用类似C的API返回一个指向堆数组的原始指针。
`std::unique_ptr`是C++11中表示专有所有权的方法但是其最吸引人的功能之一是它可以轻松高效的转换为`std::shared_ptr`
`std::unique_ptr`是C++11中表示专有所有权的方法但是其最吸引人的功能之一是它可以轻松高效的转换为`std::shared_ptr`
```cpp
std::shared_ptr<Investment> sp = makeInvestment(arguments);
```
这就是为什么std :: unique_ptr非常适合用作工厂函数返回类型的关键部分。 工厂函数无法知道调用者是否要对它们返回的对象使用专有所有权语义或者共享所有权即std :: shared_ptr是否更合适。 通过返回std :: unique_ptr工厂为调用者提供了最有效的智能指针但它们并不妨碍调用者用其更灵活的兄弟替换它。 有关std :: shared_ptr的信息请转到Item 19
这就是为什么`std::unique_ptr`非常适合用作工厂函数返回类型的关键部分。 工厂函数无法知道调用者是否要对它们返回的对象使用专有所有权语义,或者共享所有权(即`std::shared_ptr`)是否更合适。 通过返回`std::unique_ptr`,工厂为调用者提供了最有效的智能指针,但它们并不妨碍调用者用其更灵活的兄弟替换它。(有关`std::shared_ptr`的信息请转到Item 19
### 小结

View File

@ -1,14 +1,14 @@
## Item 19:对于共享资源使用std::shared_ptr
条款十九:对于共享资源使用std::shared_ptr
程序员使用带垃圾回收的语言指着C++笑看他们如何防止资源泄露。“真是原始啊”他们嘲笑着说。“你们没有从1960年的Lisp那里得到启发吗机器应该自己管理资源的生命周期而不应该依赖人类。”C++程序眼滚动眼珠。“你得到的启发就是只有内存算资源其他资源释放都是非确定性的你知道吗我们更喜欢通用可预料的销毁谢谢你。”但我们的虚张声势可能底气不足。因为垃圾回收真的很方便而且手动管理生命周期真的就像是使用石头小刀和兽皮制作RAM电路。为什么我们不能同时有两个完美的世界一个自动工作的世界垃圾回收一个销毁可预测的世界析构
程序员使用带垃圾回收的语言指着C++笑看他们如何防止资源泄露。“真是原始啊”他们嘲笑着说。“你们没有从1960年的Lisp那里得到启发吗机器应该自己管理资源的生命周期而不应该依赖人类。”C++程序员翻白眼。“你得到的启发就是只有内存算资源其他资源释放都是非确定性的你知道吗我们更喜欢通用可预料的销毁谢谢你。”但我们的虚张声势可能底气不足。因为垃圾回收真的很方便而且手动管理生命周期真的就像是使用石头小刀和兽皮制作RAM电路。为什么我们不能同时有两个完美的世界一个自动工作的世界垃圾回收一个销毁可预测的世界析构
C++11中的`std::shared_ptr`将两者组合了起来。一个通过`std::shared_ptr`访问的对象其生命周期由指向它的指针们共享所有权shared ownership。没有特定的`std::shared_ptr`拥有该对象。相反,所有指向它的`std::shared_ptr`都能相互合作确保在它不再使用的那个点进行析构。当最后一个`std::shared_ptr`到达那个点,`std::shared_ptr`会销毁它所指向的对象。就垃圾回收来说,客户端不需要关心指向对象的生命周期,而对象的析构是确定性的。
`std::shared_ptr`通过引用计数来确保它是否是最后一个指向某种资源的指针,引用计数即资源和一个值关联起来,这个值会跟踪有多少`std::shared_ptr`指向该资源。`std::shared_ptr`构造函数递增引用计数值注意是通常——原因参见下面析构函数递减值拷贝赋值运算符可能递增也可能递减值。如果sp1和sp2是`std::shared_ptr`并且指向不同对象,赋值运算符`sp1=sp2`会使sp1指向sp2指向的对象。直接效果就是sp1引用计数减一sp2引用计数加一。如果`std::shared_ptr`发现引用计数值为零,没有其他`std::shared_ptr`指向该资源,它就会销毁资源。
`std::shared_ptr`通过引用计数来确保它是否是最后一个指向某种资源的指针,引用计数关联资源并跟踪有多少`std::shared_ptr`指向该资源。`std::shared_ptr`构造函数递增引用计数值注意是通常——原因参见下面析构函数递减值拷贝赋值运算符可能递增也可能递减值。如果sp1和sp2是`std::shared_ptr`并且指向不同对象,赋值运算符`sp1=sp2`会使sp1指向sp2指向的对象。直接效果就是sp1引用计数减一sp2引用计数加一。如果`std::shared_ptr`发现引用计数值为零,没有其他`std::shared_ptr`指向该资源,它就会销毁资源。
引用计数暗示着性能问题:
+ **std::shared_ptr大小是原始指针的两倍**,因为它内部包含一个指向资源的原始指针,还包含一个资源的引用计数值
+ **`std::shared_ptr`大小是原始指针的两倍**,因为它内部包含一个指向资源的原始指针,还包含一个资源的引用计数值
+ **引用计数必须动态分配**。 理论上引用计数与所指对象关联起来但是被指向的对象不知道这件事情译注不知道有指向自己的指针。因此它们没有办法存放一个引用计数值。Item21会解释使用`std::make_shared`创建`std::shared_ptr`可以避免引用计数的动态分配,但是还存在一些`std::make_shared`不能使用的场景,这时候引用计数就会动态分配。
+ **递增递减引用计数必须是原子性的**因为多个reader、writer可能在不同的线程。比如指向某种资源的`std::shared_ptr`可能在一个线程执行析构,在另一个不同的线程,`std::shared_ptr`指向相同的对象,但是执行的确是拷贝操作。原子操作通常比非原子操作要慢,所以即使是引用计数,你也应该假定读写它们是存在开销的。
@ -51,8 +51,8 @@ std::vector<std::shared_ptr<Widget>> vpw{ pw1, pw2 };
当`std::shared_ptr`对象一创建,对象控制块就建立了。至少我们期望是如此。通常,对于一个创建指向对象的`std::shared_ptr`的函数来说不可能知道是否有其他`std::shared_ptr`早已指向那个对象,所以控制块的创建会遵循下面几条规则:
+ **std::make_shared总是创建一个控制块**(参见Item21)。它创建一个指向新对象的指针,所以可以肯定`std::make_shared`调用时对象不存在其他控制块。
+ **当从独占指针上构造出`std::shared_ptr`时会创建控制块(即std::unique_ptr或者std::auto_ptr**。独占指针没有使用控制块,所以指针指向的对象没有关联其他控制块。(作为构造的一部分,`std::shared_ptr`侵占独占指针所指向的对象的独占权,所以`std::unique_ptr`被设置为null
+ **`std::make_shared`总是创建一个控制块**(参见Item21)。它创建一个指向新对象的指针,所以可以肯定`std::make_shared`调用时对象不存在其他控制块。
+ **当从独占指针上构造出`std::shared_ptr`时会创建控制块(即`std::unique_ptr`或者`std::auto_ptr`**。独占指针没有使用控制块,所以指针指向的对象没有关联其他控制块。(作为构造的一部分,`std::shared_ptr`侵占独占指针所指向的对象的独占权,所以`std::unique_ptr`被设置为null
+ **当从原始指针上构造出`std::shared_ptr`时会创建控制块**。如果你想从一个早已存在控制块的对象上创建`std::shared_ptr`,你将假定传递一个`std::shared_ptr`或者`std::weak_ptr`作为构造函数实参,而不是原始指针。用`std::shared_ptr`或者`std::weak_ptr`作为构造函数实参创建`std::shared_ptr`不会创建新控制块,因为它可以依赖传递来的智能指针指向控制块。
这些规则造成的后果就是从原始指针上构造超过一个`std::shared_ptr`就会让你走上未定义行为的快车道,因为指向的对象有多个控制块关联。多个控制块意味着多个引用计数值,多个引用计数值意味着对象将会被销毁多次(每个引用计数一次)。那意味着下面的代码是有问题的,很有问题,问题很大:
@ -63,14 +63,14 @@ std::shared_ptr<Widget> spw1(pw, loggingDel); // 为*pw创建控制块
std::shared_ptr<Widget> spw2(pw, loggingDel); // 为*pw创建第二个控制块
```
创建原始指针指向动态分配的对象很糟糕因为它完全背离了这章的建议对于共享资源使用std::shared_ptr而不是原始指针。如果你忘记了该建议的动机请翻到115页。撇开那个不说创建**pw**那一行代码虽然让人厌恶,但是至少不会造成未定义程序行为。
创建原始指针指向动态分配的对象很糟糕,因为它完全背离了这章的建议:对于共享资源使用`std::shared_ptr`而不是原始指针。如果你忘记了该建议的动机请翻到115页。撇开那个不说创建**pw**那一行代码虽然让人厌恶,但是至少不会造成未定义程序行为。
现在,传给**spw1**的构造函数一个原始指针,它会为指向的对象创建一个控制块(引用计数值在里面)。这种情况下,指向的对象是`*pw`。就其本身而言没什么问题,但是将同样的原始指针传递给**spw2**的构造函数会再次为`*pw`创建一个控制块。因此`*pw`有两个引用计数值,每一个最后都会变成零,然后最终导致`*pw`销毁两次。第二个销毁会产生未定义行为。
`std::shared_ptr`给我们上了两堂课。第一,避免传给`std::shared_ptr`构造函数原始指针。通常替代方案是使用`std::make_shared`(参见Item21),不过上面例子中,我们使用了自定义销毁器,用`std::make_shared`就没办法做到。第二,如果你必须传给`std::shared_ptr`构造函数原始指针直接传new出来的结果不要传指针变量。如果上面代码第一部分这样重写
```cpp
std::shared_ptr<Widget> spw1(new Widget, // 直接使用new的结果
loggingDel);
loggingDel);
```
会少了很多创建第二个从原始指针上构造`std::shared_ptr`的诱惑。相应的创建spw2也会很自然的用spw1作为初始化参数即用`std::shared_ptr`拷贝构造),那就没什么问题了:
```CPP
@ -150,7 +150,7 @@ private:
`std::shared_ptr`不能处理的另一个东西是数组。和`std::unique_ptr`不同的是,`std::shared_ptr`的API设计之初就是针对单个对象的没有办法`std::shared_ptr<T[]>`。一次又一次,“聪明”的程序员踌躇于是否该使用`std::shared_ptr<T>`指向数组,然后传入自定义数组销毁器。(即`delete []`)。这可以通过编译,但是是一个糟糕的注意。一方面,`std::shared_ptr`没有提供`operator[]`重载,所以数组索引操作需要借助怪异的指针算术。另一方面,`std::shared_ptr`支持转换为指向基类的指针,这对于单个对象来说有效,但是当用于数组类型时相当于在类型系统上开洞。(出于这个原因,`std::unique_ptr`禁止这种转换。。更重要的是C++11已经提供了很多内置数组的候选方案比如`std::array`,`std::vector`,`std::string`)。声明一个指向傻瓜数组的智能指针几乎总是标识着糟糕的设计。
记住:
+ `std::shared_ptr`为任意共享所有权的资源一种自动垃圾回收的便捷方式
+ 较之于`std::unique_ptr``std::shared_ptr`对象通常大两倍,控制块会产生开销,需要原子引用计数修改操作
+ 默认资源销毁是通过**delete**,但是也支持自定义销毁器。销毁器的类型是什么对于`std::shared_ptr`的类型没有影响
+ 避免从原始指针变量上创建`std::shared_ptr`
+ `std::shared_ptr`为任意共享所有权的资源一种自动垃圾回收的便捷方式
+ 较之于`std::unique_ptr``std::shared_ptr`对象通常大两倍,控制块会产生开销,需要原子引用计数修改操作
+ 默认资源销毁是通过**delete**,但是也支持自定义销毁器。销毁器的类型是什么对于`std::shared_ptr`的类型没有影响
+ 避免从原始指针变量上创建`std::shared_ptr`

View File

@ -1,7 +1,7 @@
## Item 20:像std::shared_ptr一样使用std::weak_ptr可能造成dangle
## Item 20: 当std::shard_ptr可能悬空时使用std::weak_ptr
自相矛盾的是,如果有一个像`std::shared_ptr`的指针但是不参与资源所有权共享的指针是很方便的。换句话说,类似`std::shared_ptr`的指针不影响对象引用计数。这种类型的智能指针必须要解决一个`std::shared_ptr`不存在的问题:可能指向已经销毁的对象。一个真正的智能指针应该跟踪所值对象,在dangle时知晓比如当指向对象不再存在。那就是对`std::weak_ptr`最精确的描述。
自相矛盾的是,如果有一个像`std::shared_ptr`的指针但是不参与资源所有权共享的指针是很方便的。换句话说,是一个类似`std::shared_ptr`但不影响对象引用计数的指针。这种类型的智能指针必须要解决一个`std::shared_ptr`不存在的问题:可能指向已经销毁的对象。一个真正的智能指针应该跟踪所值对象,在悬空时知晓,悬空(*dangle*)就是指针指向的对象不再存在。这就是对`std::weak_ptr`最精确的描述。
你可能想知道什么时候该用`std::weak_ptr`。你可能想知道关于`std::weak_ptr`API的更多。它什么都好除了不太智能。`std::weak_ptr`不能解引用,也不能测试是否为空值。因为`std::weak_ptr`不是一个独立的智能指针。它是`std::shared_ptr`的增强。
@ -41,15 +41,17 @@ std::shared_ptr<Widget> spw3(wpw); // if wpw's expired, throw std::bad_weak_pt
```
如果调用`loadWidget`是一个昂贵的操作比如它操作文件或者数据库I/O并且对于ID来重复使用很常见一个合理的优化是再写一个函数除了完成`loadWidget`做的事情之外再缓存它的结果。当请求获取一个Widget时阻塞在缓存操作上这本身也会导致性能问题所以另一个合理的优化可以是当Widget不再使用的时候销毁它的缓存。
对于可缓存的工厂函数,返回`std::unique_ptr`不是好的选择。调用者接受缓存后的对象的只能指针,调用者也应该确定这些对象的生命周期,但是缓存本身也需要一个指针指向它所缓的对象。缓存对象的指针需要知道它是否已经dangle,因为当工厂客户端使用完工厂产生的对象后,对象将被销毁,关联的缓存条目会dangle。所以缓存应该使用`std::weak_ptr`这可以知道是否已经dangle。这意味着工厂函数返回值类型应该是`std::shared_ptr`,因为`std::weak_ptr`依赖`std::shared_ptr`
对于可缓存的工厂函数,返回`std::unique_ptr`不是好的选择。调用者应该接收缓存对象的智能指针,调用者也应该确定这些对象的生命周期,但是缓存本身也需要一个指针指向它所缓的对象。缓存对象的指针需要知道它是否已经悬空,因为当工厂客户端使用完工厂产生的对象后,对象将被销毁,关联的缓存条目会悬空。所以缓存应该使用`std::weak_ptr`,这可以知道是否已经悬空。这意味着工厂函数返回值类型应该是`std::shared_ptr`,因为只有当对象的生命周期由`std::shared_ptr`管理时,`std::weak_ptr`才能检测到悬空
下面是一个粗制滥造的缓存版本的`loadWidget`实现:
下面是一个临时凑合的`loadWidget`的缓存版本的实现:
```cpp
std::shared_ptr<const Widget> fastLoadWidget(WidgetID id)
{
static std::unordered_map<WidgetID,
std::weak_ptr<const Widget>> cache; // 译者注:这里是高亮
auto objPtr = cache[id].lock(); // objPtr is std::shared_ptr to cached object (or null if object's not in cache)
auto objPtr = cache[id].lock(); // objPtr is std::shared_ptr
// to cached object
// (or null if object's not in cache)
if (!objPtr) { // if not in cache
objPtr = loadWidget(id); // load it
cache[id] = objPtr; // cache it
@ -58,11 +60,11 @@ std::shared_ptr<const Widget> fastLoadWidget(WidgetID id)
}
```
这个实现使用了C++11的hash表容器`std::unordered_map`尽管没有显式表明需要`WidgetID`哈希和相等性比较的能力
这个实现使用了C++11的hash表容器`std::unordered_map`但是需要的`WidgetID`哈希和相等性比较函数在这里没有展示
`fastLoadWidget`的实现忽略了以下事实cache可能会累积`expired`的与已经销毁的`Widget`相关联的`std::weak_ptr`。可以改进实现方式,但不要花时间在不会引起对`std :: weak_ptr`的深入了解的问题上让我们考虑第二个用例观察者设计模式。此模式的主要组件是subjects状态可能会更改的对象和observers状态发生更改时要通知的对象。在大多数实现中每个subject都包含一个数据成员该成员持有指向其observer的指针。这使subject很容易发布状态更改通知。subject对控制observers的生命周期例如当它们被销毁时没有兴趣但是subject对确保observers被销毁时不会访问它具有极大的兴趣 。一个合理的设计是每个subject持有其observers的`std::weak_ptr`,因此可以在使用前检查是否已经dangle
`fastLoadWidget`的实现忽略了以下事实cache可能会累积过期的`std::weak_ptr`(对应已经销毁的`Widget`)。可以改进实现方式,但不要花时间在不会引起对`std :: weak_ptr`的深入了解的问题上让我们考虑第二个用例观察者设计模式。此模式的主要组件是subjects状态可能会更改的对象和observers状态发生更改时要通知的对象。在大多数实现中每个subject都包含一个数据成员该成员持有指向其observer的指针。这使subject很容易发布状态更改通知。subject对控制observers的生命周期例如当它们被销毁时没有兴趣但是subject对确保observers被销毁时不会访问它具有极大的兴趣 。一个合理的设计是每个subject持有其observers的`std::weak_ptr`,因此可以在使用前检查是否已经悬空
作为最后一个使用`std::weak_ptr`的例子考虑一个持有三个对象A,B,C的数据结构A和C共享B的所有权因此持有`std::shared_ptr`
作为最后一个使用`std::weak_ptr`的例子考虑一个持有三个对象A、B、C的数据结构A和C共享B的所有权因此持有`std::shared_ptr`
![image-20201101170753295](media/item20/image-20201101170753295.png)
@ -72,9 +74,9 @@ std::shared_ptr<const Widget> fastLoadWidget(WidgetID id)
有三种选择:
- **原始指针**。使用这种方法如果A被销毁但是C继续指向BB就会有一个指向A的悬垂指针。而且B不知道指针已经悬垂所以B可能会继续访问就会导致未定义行为
- **`std::shared_ptr`**。这种设计A和B都互相持有对方的`std::shared_ptr`,导致`std::shared_ptr`在销毁时出现循环。即使A和B无法从其他数据结构被访问比如C不再指向B每个的引用计数都是1.如果发升了这种情况A和B都被泄露程序无法访问它们但是资源并没有被回收。
- **`std::weak_ptr`**。这避免了上述两个问题。如果A被销毁B还是有dangle指针但是B可以检查。尤其是尽管A和B互相指向B的指针不会影响A的引用计数因此不会导致无法销毁。
- **原始指针**。使用这种方法如果A被销毁但是C继续指向BB就会有一个指向A的悬空指针。而且B不知道指针已经悬空所以B可能会继续访问就会导致未定义行为
- **`std::shared_ptr`**。这种设计A和B都互相持有对方的`std::shared_ptr`,导致`std::shared_ptr`在销毁时出现循环。即使A和B无法从其他数据结构被访问比如C不再指向B每个的引用计数都是1。如果发生了这种情况A和B都被泄露程序无法访问它们但是资源并没有被回收。
- **`std::weak_ptr`**。这避免了上述两个问题。如果A被销毁B还是有悬空指针但是B可以检查。尤其是尽管A和B互相指向B的指针不会影响A的引用计数因此不会导致无法销毁。
使用`std::weak_ptr`显然是这些选择中最好的。但是,需要注意使用`std::weak_ptr`打破`std::shared_ptr`循环并不常见。在严格分层的数据结构比如树,子节点只被父节点持有。当父节点被销毁时,子节点就被销毁。从父到子的链接关系可以使用`std::unique_ptr`很好的表征。从子到父的反向连接可以使用原始指针安全实现,因此子节点的生命周期肯定短于父节点。因此子节点解引用一个悬垂的父节点指针是没有问题的。
@ -84,5 +86,5 @@ std::shared_ptr<const Widget> fastLoadWidget(WidgetID id)
### 记住
- 像`std::shared_ptr`使用`std::weak_ptr`可能会dangle
- `std::weak_ptr`的潜在使用场景包括caching、observer lists、打破`std::shared_ptr`指向循环
- 像`std::shared_ptr`使用`std::weak_ptr`可能会悬空。
- `std::weak_ptr`的潜在使用场景包括caching、observer lists、打破`std::shared_ptr`指向循环

View File

@ -1,6 +1,6 @@
## Item 21:优先考虑使用std::make_unique和std::make_shared而非new
让我们先对std::make_unique和std::make_shared做个铺垫。std::make_shared 是C++11标准的一部分但很可惜的是std::make_unique不是。它从C++14开始加入标准库。如果你在使用C++11不用担心一个基础版本的std::make_unique是很容易自己写出的如下
让我们先对`std::make_unique`和`std::make_shared`做个铺垫。`std::make_shared`是C++11标准的一部分但很可惜的是`std::make_unique`不是。它从C++14开始加入标准库。如果你在使用C++11不用担心一个基础版本的`std::make_unique`是很容易自己写出的,如下:
```cpp
template<typename T, typename... Ts>
@ -10,7 +10,7 @@ std::unique_ptr<T> make_unique(Ts&&... params)
}
```
正如你看到的make_unique只是将它的参数完美转发到所要创建的对象的构造函数从新产生的原始指针里面构造出std::unique_ptr并返回这个std::unique_ptr。这种形式的函数不支持数组和自定义析构但它给出了一个示范只需一点努力就能写出你想要的make_uniqe函数。需要记住的是不要把它放到std命名空间中因为你可能并不希望在升级厂家编译器到符合C++14标准的时候产生冲突。
正如你看到的,`make_unique`只是将它的参数完美转发到所要创建的对象的构造函数,从新产生的原始指针里面构造出`std::unique_ptr`,并返回这个`std::unique_ptr`。这种形式的函数不支持数组和自定义析构,但它给出了一个示范:只需一点努力就能写出你想要的`make_uniqe`函数。需要记住的是不要把它放到std命名空间中因为你可能并不希望在升级厂家编译器到符合C++14标准的时候产生冲突。
`std::make_unique`和`std::make_shared`有三个make functions中的两个接收抽象参数完美转发到构造函数去动态分配一个对象然后返回这个指向这个对象的指针。第三个make function 是`std::allocate_shared.`它和`std::make_shared`一样,除了第一个参数是用来动态分配内存的对象。
@ -31,7 +31,7 @@ std::shared_ptr<Widget> spw2(new Widget); // without make func
void processWidget(std::shared_ptr<Widget> spw, int priority);
```
根据值传递std::shared ptr可能看起来很可疑但是Item 41解释了如果processWidget总是复制std::shared ptr(例如通过将其存储在已处理的Widget的数据结构中),那么这可能是一个可复用的设计选择。
根据值传递`std::shared_ptr`可能看起来很可疑但是Item 41解释了如果processWidget总是复制`std::shared_ptr`(例如通过将其存储在已处理的Widget的数据结构中),那么这可能是一个可复用的设计选择。
现在假设我们有一个函数来计算相关的优先级
@ -43,7 +43,7 @@ void processWidget(std::shared_ptr<Widget> spw, int priority);
processWidget(std::shared_ptr<Widget>(new Widget), computePriority()); // potential resource leak!
```
如注释所说这段代码可能在new Widget时发生泄露。为何调用的代码和被调用的函数都用std::shared_ptrs,且std::shared_ptrs就是设计出来防止泄露的。它们会在最后一个std::shared_ptr销毁时自动释放所指向的内存。如果每个人在每个地方都用std::shared_ptrs,这段代码怎么会泄露呢?
如注释所说这段代码可能在new Widget时发生泄露。为何调用的代码和被调用的函数都用`std::shared_ptr`s,且`std::shared_ptr`s就是设计出来防止泄露的。它们会在最后一个`std::shared_ptr`销毁时自动释放所指向的内存。如果每个人在每个地方都用`std::shared_ptr`s,这段代码怎么会泄露呢?
答案和编译器将源码转换为目标代码有关。在运行时一个函数的参数必须先被计算才能被调用所以在调用processWidget之前必须执行以下操作processWidget才开始执行
@ -51,42 +51,42 @@ processWidget(std::shared_ptr<Widget>(new Widget), computePriority()); // potent
- 负责管理new出来指针的`std::shared_ptr<Widget>`构造函数必须被执行
- computePriority()必须运行
编译器不需要按照执行顺序生成代码。“new Widget"必须在std::shared_ptr的构造函数被调用前执行因为new出来的结果作为构造函数的参数但compute Priority可能在这之前之后或者之间执行。也就是说编译器可能按照这个执行顺序生成代码
编译器不需要按照执行顺序生成代码。“new Widget"必须在`std::shared_ptr`的构造函数被调用前执行因为new出来的结果作为构造函数的参数但compute Priority可能在这之前之后或者之间执行。也就是说编译器可能按照这个执行顺序生成代码
1. 执行new Widget
2. 执行computePriority
3. 运行std::shared_ptr构造函数
3. 运行`std::shared_ptr`构造函数
如果按照这样生成代码并且在运行是computePriority产生了异常那么第一步动态分配的Widget就会泄露。因为它永远都不会被第三步的std::shared_ptr所管理了。
如果按照这样生成代码并且在运行是computePriority产生了异常那么第一步动态分配的Widget就会泄露。因为它永远都不会被第三步的`std::shared_ptr`所管理了。
使用std::make_shared可以防止这种问题。调用代码看起来像是这样
使用`std::make_shared`可以防止这种问题。调用代码看起来像是这样:
```c++
processWidget(std::make_shared<Widget>(), computePriority());
```
在运行时std::make_shared和computePriority会先被调用。如果是std::make_shared在computePriority调用前动态分配Widget的原始指针会安全的保存在作为返回值的std::shared_ptr中。如果compu tePriority生成一个异常那么std::shared_ptr析构函数将确保管理的Widget被销毁。如果首先调用computePriority并产生一个异常那么std::make_shared将不会被调用因此也就不需要担心new Widget(会泄露)。
在运行时,`std::make_shared`和computePriority会先被调用。如果是`std::make_shared`在computePriority调用前动态分配Widget的原始指针会安全的保存在作为返回值的`std::shared_ptr`中。如果compu tePriority生成一个异常那么`std::shared_ptr`析构函数将确保管理的Widget被销毁。如果首先调用computePriority并产生一个异常那么`std::make_shared`将不会被调用因此也就不需要担心new Widget(会泄露)。
如果我们将std::shared_ptr,std::make_shared替换成std::unique_ptr,std::make_unique,同样的道理也适用。因此在编写异常安全代码时使用std::make_unique而不是new与使用std::make_shared同样重要。
如果我们将`std::shared_ptr`,`std::make_shared`替换成std::unique_ptr,std::make_unique,同样的道理也适用。因此在编写异常安全代码时使用std::make_unique而不是new与使用`std::make_shared`同样重要。
std::make_shared的一个特性(与直接使用new相比)得到了效率提升。使用std::make_shared允许编译器生成更小更快的代码并使用更简洁的数据结构。考虑以下对new的直接使用
`std::make_shared`的一个特性(与直接使用new相比)得到了效率提升。使用`std::make_shared`允许编译器生成更小更快的代码并使用更简洁的数据结构。考虑以下对new的直接使用
```c++
std::shared_ptr<Widget> spw(new Widget);
```
显然,这段代码需要进行内存分配,但它实际上执行了两次.Item 19解释了每个std::shared_ptr指向一个控制块其中包含被指向对象的引用计数。这个控制块的内存在std::shared_ptr构造函数中分配。因此直接使用new需要为Widget分配一次内存为控制块分配再分配一次内存。
显然,这段代码需要进行内存分配,但它实际上执行了两次。Item 19解释了每个`std::shared_ptr`指向一个控制块,其中包含被指向对象的引用计数。这个控制块的内存在`std::shared_ptr`构造函数中分配。因此直接使用new需要为Widget分配一次内存为控制块分配再分配一次内存。
如果使用std::make_shared代替` auto spw = std::make_shared_ptr<Widget>();`一次分配足矣。这是因为std::make_shared分配一块内存同时容纳了Widget对象和控制块。这种优化减少了程序的静态大小因为代码只包含一个内存分配调用并且它提高了可执行代码的速度因为内存只分配一次。此外使用std::make_shared避免了对控制块中的某些簿记信息的需要潜在地减少了程序的总内存占用。
如果使用`std::make_shared`代替:` auto spw = std::make_shared_ptr<Widget>();`一次分配足矣。这是因为`std::make_shared`分配一块内存同时容纳了Widget对象和控制块。这种优化减少了程序的静态大小因为代码只包含一个内存分配调用并且它提高了可执行代码的速度因为内存只分配一次。此外使用`std::make_shared`避免了对控制块中的某些簿记信息的需要,潜在地减少了程序的总内存占用。
对于std::make_shared的效率分析同样适用于std::allocate_shared因此std::make_shared的性能优势也扩展到了该函数。
对于`std::make_shared`的效率分析同样适用于`std::allocate_shared`,因此`std::make_shared`的性能优势也扩展到了该函数。
更倾向于使用函数而不是直接使用new的争论非常激烈。尽管它们在软件工程、异常安全和效率方面具有优势但本item的意见是更倾向于使用make函数而不是完全依赖于它们。这是因为有些情况下它们不能或不应该被使用。
例如没有make函数允许指定定制的析构(见item18和19),但是std::unique_ptr和std::shared_ptr有构造函数这么做。给Widget自定义一个析构:
```
例如没有make函数允许指定定制的析构(见item18和19),但是`std::unique_ptr`和`std::shared_ptr`有构造函数这么做。给Widget自定义一个析构:
```cpp
auto widgetDeleter = [](Widget*){...};
```
使用new创建智能指针非常简单:
```
```cpp
std::unique_ptr<Widget, decltype(widgetDeleter)>
upw(new Widget, widgetDeleter);
@ -94,33 +94,33 @@ std::shared_ptr<Widget> spw(new Widget, widgetDeleter);
```
对于make函数没有办法做同样的事情。
make函数第二个限制来自于其单一概念的句法细节。Item7解释了当构造函数重载有std::initializer_list作为参数和不用其作为参数时用大括号创建对象更倾向于使用std::initializer_list作为参数的构造函数而用圆括号创建对象倾向于不用std::initializer_list作为参数的构造函数。make函数会将它们的参数完美转发给对象构造函数但是它们是使用圆括号还是大括号对某些类型问题的答案会很不相同。例如在这些调用中
```
make函数第二个限制来自于其单一概念的句法细节。Item7解释了当构造函数重载`std::initializer_list`作为参数和不用其作为参数时,用大括号创建对象更倾向于使用`std::initializer_list`作为参数的构造函数,而用圆括号创建对象倾向于不用`std::initializer_list`作为参数的构造函数。make函数会将它们的参数完美转发给对象构造函数但是它们是使用圆括号还是大括号对某些类型问题的答案会很不相同。例如在这些调用中
```cpp
auto upv = std::make_unique<std::vector<int>>(10, 20);
auto spv = std::make_shared<std::vector<int>>(10, 20);
```
生成的智能指针是否指向带有10个元素的std::vector每个元素值为20或指向带有两个元素的std::vector其中一个元素值10另一个为20 ?或者结果是不确定的?
生成的智能指针是否指向带有10个元素的`std::vector`每个元素值为20或指向带有两个元素的`std::vector`其中一个元素值10另一个为20 ?或者结果是不确定的?
好消息是这并非不确定两种调用都创建了10个元素每个值为20.这意味着在make函数中完美转发使用圆括号而不是大括号。坏消息是如果你想用大括号初始化指向的对象你必须直接使用new。使用make函数需要能够完美转发大括号初始化但是正如item31所说大括号初始化无法完美转发。但是item30介绍了一个变通的方法使用auto类型推导从大括号初始化创建std::initializer_list对象(见Item 2)然后将auto创建的对象传递给make函数。
```
```cpp
// create std::initializer_list
auto initList = { 10, 20 };
// create std::vector using std::initializer_list ctor
auto spv = std::make_shared<std::vector<int>>(initList);
```
对于std::unique_ptr,只有这两种情景定制删除和大括号初始化使用make函数有点问题。对于std::shared_ptr和它的make函数还有至少2个问题。都属于边界问题但是一些开发者常碰到你也可能是其中之一。
对于std::unique_ptr,只有这两种情景定制删除和大括号初始化使用make函数有点问题。对于`std::shared_ptr`和它的make函数还有至少2个问题。都属于边界问题但是一些开发者常碰到你也可能是其中之一。
一些类重载了operator new和operator delete。这些函数的存在意味着对这些类型的对象的全局内存分配和释放是不合常规的。设计这种定制类往往只会精确的分配、释放对象的大小。例如Widget类的operator new和operator delete只会处理sizeof(Widget)大小的内存块的分配和释放。这种常识不太适用于std::shared_ptr对定制化分配(通过std::allocate_shared)和释放(通过定制化deleters)因为std::allocate_shared需要的内存总大小不等于动态分配的对象大小还需要再加上控制块大小。因此适用make函数去创建重载了operator new 和 operator delete类的对象是个典型的糟糕想法。
一些类重载了operator new和operator delete。这些函数的存在意味着对这些类型的对象的全局内存分配和释放是不合常规的。设计这种定制类往往只会精确的分配、释放对象的大小。例如Widget类的operator new和operator delete只会处理sizeof(Widget)大小的内存块的分配和释放。这种常识不太适用于`std::shared_ptr`对定制化分配(通过std::allocate_shared)和释放(通过定制化deleters)因为std::allocate_shared需要的内存总大小不等于动态分配的对象大小还需要再加上控制块大小。因此适用make函数去创建重载了operator new 和 operator delete类的对象是个典型的糟糕想法。
与直接使用new相比std::make_shared在大小和速度上的优势源于std::shared_ptr的控制块与指向的对象放在同一块内存中。当对象的引用计数降为0对象被销毁(析构函数被调用).但是,因为控制块和对象被放在同一块分配的内存块中,直到控制块的内存也被销毁,它占用的内存是不会被释放的。
与直接使用new相比`std::make_shared`在大小和速度上的优势源于`std::shared_ptr`的控制块与指向的对象放在同一块内存中。当对象的引用计数降为0对象被销毁(析构函数被调用).但是,因为控制块和对象被放在同一块分配的内存块中,直到控制块的内存也被销毁,它占用的内存是不会被释放的。
正如我说控制块除了引用计数还包含簿记信息。引用计数追踪有多少std::shared_ptrs指向控制块但控制块还有第二个计数记录多少个std::weak_ptrs指向控制块。第二个引用计数就是weak count。当一个std::weak_ptr检测对象是否过期时(见item 19),它会检测指向的控制块中的引用计数(而不是weak count)。如果引用计数是0(即对象没有std::shared_ptr再指向它已经被销毁了)std::weak_ptr已经过期。否则就没过期。
正如我说,控制块除了引用计数,还包含簿记信息。引用计数追踪有多少`std::shared_ptr`s指向控制块但控制块还有第二个计数记录多少个std::weak_ptrs指向控制块。第二个引用计数就是weak count。当一个std::weak_ptr检测对象是否过期时(见item 19),它会检测指向的控制块中的引用计数(而不是weak count)。如果引用计数是0(即对象没有`std::shared_ptr`再指向它,已经被销毁了)std::weak_ptr已经过期。否则就没过期。
只要std::weak_ptrs引用一个控制块(即weak count大于零)该控制块必须继续存在。只要控制块存在包含它的内存就必须保持分配。通过std::shared_ptr make函数分配的内存直到最后一个std::shared_ptr和最后一个指向它的std::weak_ptr已被销毁才会释放。
只要std::weak_ptrs引用一个控制块(即weak count大于零),该控制块必须继续存在。只要控制块存在,包含它的内存就必须保持分配。通过`std::shared_ptr` make函数分配的内存直到最后一个`std::shared_ptr`和最后一个指向它的std::weak_ptr已被销毁才会释放。
如果对象类型非常大而且销毁最后一个std::shared_ptr和销毁最后一个std::weak_ptr之间的时间很长那么在销毁对象和释放它所占用的内存之间可能会出现延迟。
如果对象类型非常大,而且销毁最后一个`std::shared_ptr`和销毁最后一个std::weak_ptr之间的时间很长那么在销毁对象和释放它所占用的内存之间可能会出现延迟。
```c++
class ReallyBigType { … };
@ -140,7 +140,7 @@ auto pBigObj = std::make_shared<ReallyBigType>();
// 控制块和对象的内存被释放
```
直接只用new一旦最后一个std::shared_ptr被销毁ReallyBigType对象的内存就会被释放
直接只用new一旦最后一个`std::shared_ptr`被销毁ReallyBigType对象的内存就会被释放
```c++
class ReallyBigType { … };
@ -162,7 +162,7 @@ std::shared_ptr<ReallyBigType> pBigObj(new ReallyBigType);
// 控制块内存被释放
```
如果你发现自己处于不可能或不合适使用std::make_shared的情况下你将想要保证自己不受我们之前看到的异常安全问题的影响。最好的方法是确保在直接使用new时在一个不做其他事情的语句中立即将结果传递到智能指针构造函数。这可以防止编译器生成的代码在使用new和调用管理新对象的智能指针的构造函数之间发生异常。
如果你发现自己处于不可能或不合适使用`std::make_shared`的情况下你将想要保证自己不受我们之前看到的异常安全问题的影响。最好的方法是确保在直接使用new时在一个不做其他事情的语句中立即将结果传递到智能指针构造函数。这可以防止编译器生成的代码在使用new和调用管理新对象的智能指针的构造函数之间发生异常。
例如考虑我们前面讨论过的processWidget函数对其非异常安全调用的一个小修改。这一次我们将指定一个自定义删除器:
```c++
@ -178,14 +178,14 @@ processWidget(
computePriority()
);
```
回想一下:如果computePriority在“new Widget”之后而在std::shared_ptr构造函数之前调用并且如果computePriority产生一个异常那么动态分配的Widget将会泄漏。
回想一下:如果computePriority在“new Widget”之后而在`std::shared_ptr`构造函数之前调用并且如果computePriority产生一个异常那么动态分配的Widget将会泄漏。
这里使用自定义删除排除了对std::make_shared的使用因此避免这个问题的方法是将Widget的分配和std::shared_ptr的构造放入它们自己的语句中然后使用得到的std::shared_ptr调用processWidget。这是该技术的本质不过正如我们稍后将看到的我们可以对其进行调整以提高其性能
这里使用自定义删除排除了对`std::make_shared`的使用因此避免这个问题的方法是将Widget的分配和`std::shared_ptr`的构造放入它们自己的语句中,然后使用得到的`std::shared_ptr`调用processWidget。这是该技术的本质不过正如我们稍后将看到的我们可以对其进行调整以提高其性能
```c++
std::shared_ptr<Widget> spw(new Widget, cusDel);
processWidget(spw, computePriority()); // 正确,但是没优化,见下
```
这是可行的因为std::shared_ptr假定了传递给它的构造函数的原始指针的所有权即使构造函数产生了一个异常。此例中如果spw的构造函数抛出异常(即无法为控制块动态分配内存)仍然能够保证cusDel会在new Widget产生的指针上调用。
这是可行的,因为``std::shared_ptr``假定了传递给它的构造函数的原始指针的所有权即使构造函数产生了一个异常。此例中如果spw的构造函数抛出异常(即无法为控制块动态分配内存)仍然能够保证cusDel会在new Widget产生的指针上调用。
一个小小的性能问题是在异常不安全调用中我们将一个右值传递给processWidget
```c++
@ -198,7 +198,7 @@ processWidget(
```c++
processWidget(spw, computePriority()); //spw是左值
```
因为processWidget的std::shared_ptr参数是传值传右值给构造函数只需要move而传递左值需要拷贝。对std::shared_ptr而言这种区别是有意义的因为拷贝std::shared_ptr需要对引用计数原子加move则不需要对引用计数有操作。为了使异常安全代码达到异常不安全代码的性能水平我们需要用std::move将spw转换为右值.
因为processWidget的`std::shared_ptr`参数是传值传右值给构造函数只需要move而传递左值需要拷贝。对`std::shared_ptr`而言,这种区别是有意义的,因为拷贝`std::shared_ptr`需要对引用计数原子加move则不需要对引用计数有操作。为了使异常安全代码达到异常不安全代码的性能水平我们需要用std::move将spw转换为右值.
```c++
processWidget(std::move(spw), computePriority());
```

View File

@ -1,6 +1,6 @@
## 当使用Pimpl惯用法请在实现文件中定义特殊成员函数
如果你曾经与过多的编译次数斗争过,你会对`Pimpl`(Pointer to implementation)惯用法很熟悉。 凭借这样一种技巧,你可以把一个**类数据成员**替换成一个指向包含具体实现的类(或者结构体), 将放在主类(primary class)的数据成员们移动到实现类去(implementation class), 而这些数据成员的访问将通过指针间接访问。 举个例子,假如有一个类`Widget`看起来如下:
如果你曾经与过多的编译次数斗争过,你会对`Pimpl`(Pointer to implementation)惯用法很熟悉。 凭借这样一种技巧,你可以将一个**类数据成员**替换成一个指向包含具体实现的类或结构体的指针, 并将放在主类(primary class)的数据成员们移动到实现类去(implementation class), 而这些数据成员的访问将通过指针间接访问。 举个例子,假如有一个类`Widget`看起来如下:
```cpp
class Widget() //定义在头文件`widget.h`