mirror of
https://github.com/CnTransGroup/EffectiveModernCppChinese.git
synced 2025-03-24 08:10:19 +08:00
Merge pull request #75 from neko-horikawaraiko/neko-horikawaraiko-patch-1
修订item16-22,增加introduction
This commit is contained in:
commit
1b2f94982a
@ -70,7 +70,7 @@ constexpr int pow(int base, int exp) noexcept //C++14
|
||||
return result;
|
||||
}
|
||||
```
|
||||
`constexpr`函数限制为只能获取和返回**字面值类型**,这基本上意味着那些有了值的类型能在编译期决定。在C++11中,除了void外的所有内置类型外,还包括一些用户定义类型都可以是字面值类型,因为构造函数和其他成员函数可能是`constexpr`:
|
||||
`constexpr`函数限制为只能获取和返回**字面值类型**,这基本上意味着那些有了值的类型能在编译期决定。在C++11中,除了`void`外的所有内置类型,以及一些用户定义类型都可以是字面值类型,因为构造函数和其他成员函数可能是`constexpr`:
|
||||
|
||||
```cpp
|
||||
class Point {
|
||||
|
@ -1,20 +1,20 @@
|
||||
## Item16:让const成员函数线程安全
|
||||
条款16: 让const成员函数线程安全
|
||||
## 条款十六:让`const`成员函数线程安全
|
||||
**Item 16: Make `const` member functions thread safe**
|
||||
|
||||
如果我们在数学领域中工作,我们就会发现用一个类表示多项式是很方便的。在这个类中,使用一个函数来计算多项式的根是很有用的。也就是多项式的值为零的时候。这样的一个函数它不会更改多项式。所以,它自然被声明为const函数。
|
||||
如果我们在数学领域中工作,我们就会发现用一个类表示多项式是很方便的。在这个类中,使用一个函数来计算多项式的根是很有用的,也就是多项式的值为零的时候。这样的一个函数它不会更改多项式。所以,它自然被声明为`const`函数。
|
||||
|
||||
```c++
|
||||
class Polynomial {
|
||||
public:
|
||||
using RootsType = // 数据结构保存多项式为零的值
|
||||
std::vector<double>; // (“using” 的信息查看条款9)
|
||||
|
||||
using RootsType = //数据结构保存多项式为零的值
|
||||
std::vector<double>; //(“using” 的信息查看条款9)
|
||||
…
|
||||
RootsType roots() const;
|
||||
|
||||
…
|
||||
};
|
||||
```
|
||||
|
||||
计算多项式的根是很复杂的,因此如果不需要的话,我们就不做。如果必须做,我们肯定不会只做一次。所以,如果必须计算它们,就缓存多项式的根,然后实现`roots`来返回缓存的值。下面是最基本的实现:
|
||||
计算多项式的根是很复杂的,因此如果不需要的话,我们就不做。如果必须做,我们肯定不想再做第二次。所以,如果必须计算它们,就缓存多项式的根,然后实现`roots`来返回缓存的值。下面是最基本的实现:
|
||||
|
||||
```c++
|
||||
class Polynomial {
|
||||
@ -23,37 +23,38 @@ public:
|
||||
|
||||
RootsType roots() const
|
||||
{
|
||||
if (!rootsAreVaild) { // 如果缓存不可用
|
||||
// 计算根
|
||||
rootsAreVaild = true; // 用`rootVals`存储它们
|
||||
if (!rootsAreValid) { //如果缓存不可用
|
||||
… //计算根
|
||||
//用rootVals存储它们
|
||||
rootsAreValid = true;
|
||||
}
|
||||
|
||||
return rootVals;
|
||||
}
|
||||
|
||||
private:
|
||||
mutable bool rootsAreVaild{ false }; // initializers 的更多信息
|
||||
mutable RootsType rootVals{}; // 请查看条款7
|
||||
mutable bool rootsAreValid{ false }; //初始化器(initializer)的
|
||||
mutable RootsType rootVals{}; //更多信息请查看条款7
|
||||
};
|
||||
```
|
||||
|
||||
从概念上讲,`roots`并不改变它所操作的多项式对象。但是作为缓存的一部分,它也许会改变`rootVals`和`rootsAreVaild`的值。这就是`mutable`的经典使用样例,这也是为什么它是数据成员声明的一部分。
|
||||
从概念上讲,`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`中,这些线程中的一个或两个可能尝试修改成员变量`rootsAreVaild`和`rootVals`。这就意味着在没有同步的情况下,这些代码会有不同的线程读写相同的内存,这就是`data race`的定义。这段代码的行为是未定义的。
|
||||
这些用户代码是非常合理的。`roots`是`const`成员函数,那就表示着它是一个读操作。在没有同步的情况下,让多个线程执行读操作是安全的。它最起码应该做到这点。在本例中却没有做到线程安全。因为在`roots`中,这些线程中的一个或两个可能尝试修改成员变量`rootsAreValid`和`rootVals`。这就意味着在没有同步的情况下,这些代码会有不同的线程读写相同的内存,这就是数据竞争(*data race*)的定义。这段代码的行为是未定义的。
|
||||
|
||||
问题就是`roots`被声明为const,但不是线程安全的。const声明在c++11和c++98 中都是正确的(检索多项式的根并不会更改多项式的值),因此需要纠正的是线程安全的缺乏。
|
||||
问题就是`roots`被声明为`const`,但不是线程安全的。`const`声明在C++11中与在C++98中一样正确(检索多项式的根并不会更改多项式的值),因此需要纠正的是线程安全的缺乏。
|
||||
|
||||
解决这个问题最普遍简单的方法就是-------使用互斥锁:
|
||||
解决这个问题最普遍简单的方法就是——使用`mutex`(互斥量):
|
||||
```c++
|
||||
class Polynomial {
|
||||
public:
|
||||
@ -61,35 +62,36 @@ public:
|
||||
|
||||
RootsType roots() const
|
||||
{
|
||||
std::lock_guard<std::mutex> g(m); // lock mutex
|
||||
std::lock_guard<std::mutex> g(m); //锁定互斥量
|
||||
|
||||
if (!rootsAreVaild) { // 如果缓存无效
|
||||
// 计算/存储roots
|
||||
rootsAreVaild = true;
|
||||
if (!rootsAreValid) { //如果缓存无效
|
||||
… //计算/存储根值
|
||||
rootsAreValid = true;
|
||||
}
|
||||
|
||||
return rootsVals;
|
||||
} // unlock mutex
|
||||
} //解锁互斥量
|
||||
|
||||
private:
|
||||
mutable std::mutex m;
|
||||
mutable bool rootsAreVaild { false };
|
||||
mutable bool rootsAreValid { false };
|
||||
mutable RootsType rootsVals {};
|
||||
};
|
||||
```
|
||||
|
||||
`std::mutex m`被声明为`mutable`,因为锁定和解锁它的都是non-const函数。在`roots`(const成员函数)中,`m`将被视为const对象。
|
||||
值得注意的是,因为`std::mutex`是一种`move-only`的类型(一种可以移动但不能复制的类型),所以将`m`添加进多项式中的副作用是使它失去了被复制的能力。不过,它仍然可以移动。
|
||||
`std::mutex m`被声明为`mutable`,因为锁定和解锁它的都是non-`const`成员函数。在`roots`(`const`成员函数)中,`m`却被视为`const`对象。
|
||||
|
||||
在某些情况下,互斥量的副作用显会得过大。例如,如果你所做的只是计算成员函数被调用了多少次,使用`std::atomic` 修饰的counter(保证其他线程视这个操作为不可分割的发生,参见item40)通常会是一个开销更小的方法。(然而它是否轻量取决于你使用的硬件和标准库中互斥量的实现。)以下是如何使用`std::atomic`来统计调用次数。
|
||||
值得注意的是,因为`std::mutex`是一种只可移动类型(*move-only type*,一种可以移动但不能复制的类型),所以将`m`添加进`Polynomial`中的副作用是使`Polynomial`失去了被复制的能力。不过,它仍然可以移动。
|
||||
|
||||
在某些情况下,互斥量的副作用显会得过大。例如,如果你所做的只是计算成员函数被调用了多少次,使用`std::atomic` 修饰的计数器(保证其他线程视它的操作为不可分割的整体,参见[item40](https://github.com/kelthuzadx/EffectiveModernCppChinese/blob/master/7.TheConcurrencyAPI/item40.md))通常会是一个开销更小的方法。(然而它是否轻量取决于你使用的硬件和标准库中互斥量的实现。)以下是如何使用`std::atomic`来统计调用次数。
|
||||
|
||||
```c++
|
||||
class Point { // 2D point
|
||||
class Point { //2D点
|
||||
public:
|
||||
// noexcept的使用参考Item 14
|
||||
double distanceFromOrigin() const noexcept
|
||||
{
|
||||
++callCount; // 原子的递增
|
||||
…
|
||||
double distanceFromOrigin() const noexcept //noexcept的使用
|
||||
{ //参考条款14
|
||||
++callCount; //atomic的递增
|
||||
|
||||
return std::sqrt((x * x) + (y * y));
|
||||
}
|
||||
@ -100,28 +102,28 @@ private:
|
||||
};
|
||||
```
|
||||
|
||||
与`std::mutex`一样,`std::atomic`是`move-only`类型,所以在`Point`中调用`Count`的意思就是`Point`也是`move-only`的。
|
||||
与`std::mutex`一样,`std::atomic`是只可移动类型,所以在`Point`中存在`callCount`就意味着`Point`也是只可移动的。
|
||||
|
||||
因为对`std::atomic`变量的操作通常比互斥量的获取和释放的消耗更小,所以你可能会过度倾向与依赖`std::atomic`。例如,在一个类中,缓存一个开销昂贵的`int`,你就会尝试使用一对`std::atomic`变量而不是互斥锁。
|
||||
因为对`std::atomic`变量的操作通常比互斥量的获取和释放的消耗更小,所以你可能会过度倾向与依赖`std::atomic`。例如,在一个类中,缓存一个开销昂贵的`int`,你就会尝试使用一对`std::atomic`变量而不是互斥量。
|
||||
|
||||
```c++
|
||||
class Widget {
|
||||
public:
|
||||
|
||||
…
|
||||
int magicValue() const
|
||||
{
|
||||
if (cacheVaild) return cachedValue;
|
||||
if (cacheValid) return cachedValue;
|
||||
else {
|
||||
auto val1 = expensiveComputation1();
|
||||
auto val2 = expensiveComputation2();
|
||||
cachedValue = val1 + val2; // 第一步
|
||||
cacheVaild = true; // 第二步
|
||||
return cachedVaild;
|
||||
cachedValue = val1 + val2; //第一步
|
||||
cacheValid = true; //第二步
|
||||
return cachedValid;
|
||||
}
|
||||
}
|
||||
|
||||
private:
|
||||
mutable std::atomic<bool> cacheVaild{ false };
|
||||
mutable std::atomic<bool> cacheValid{ false };
|
||||
mutable std::atomic<int> cachedValue;
|
||||
};
|
||||
```
|
||||
@ -129,43 +131,43 @@ private:
|
||||
这是可行的,但难以避免有时出现重复计算的情况。考虑:
|
||||
|
||||
+ 一个线程调用`Widget::magicValue`,将`cacheValid`视为`false`,执行这两个昂贵的计算,并将它们的和分配给`cachedValue`。
|
||||
+ 此时,第二个线程调用`Widget::magicValue`,也将`cacheValid`视为`false`,因此执行刚才完成的第一个线程相同的计算。(这里的“第二个线程”实际上可能是其他几个线程。)
|
||||
+ 此时,第二个线程调用`Widget::magicValue`,也将`cacheValid`视为`false`,因此执行刚才完成的第一个线程相同的计算。(这里的“第二个线程”实际上可能是其他**几个**线程。)
|
||||
|
||||
这种行为与使用缓存的目的背道而驰。将`cachedValue`和`CacheValid`的顺序交换可以解决这个问题,但结果会更糟:
|
||||
这种行为与使用缓存的目的背道而驰。将`cachedValue`和`CacheValid`的赋值顺序交换可以解决这个问题,但结果会更糟:
|
||||
|
||||
```c++
|
||||
class Widget {
|
||||
public:
|
||||
|
||||
…
|
||||
int magicValue() const
|
||||
{
|
||||
if (cacheVaild) return cachedValue;
|
||||
if (cacheValid) return cachedValue;
|
||||
else {
|
||||
auto val1 = expensiveComputation1();
|
||||
auto val2 = expensiveComputation2();
|
||||
cacheVaild = true; // 第一步
|
||||
return cachedValue = val1 + val2; // 第二步
|
||||
cacheValid = true; //第一步
|
||||
return cachedValue = val1 + val2; //第二步
|
||||
}
|
||||
}
|
||||
|
||||
…
|
||||
}
|
||||
```
|
||||
|
||||
假设`cacheVaild`是false,那么:
|
||||
假设`cacheValid`是false,那么:
|
||||
|
||||
+ 一个线程调用`Widget::magicValue`,在`cacheVaild` 被设置成true时执行到它。
|
||||
+ 在这时,第二个线程调用`Widget::magicValue`随后检查缓存值。看到它是true,就返回`cacheValue`,即使第一个线程还没有给它赋值。因此返回的值是不正确的。
|
||||
+ 一个线程调用`Widget::magicValue`,刚执行完将`cacheValid`设置`true`的语句。
|
||||
+ 在这时,第二个线程调用`Widget::magicValue`,检查`cacheValid`。看到它是`true`,就返回`cacheValue`,即使第一个线程还没有给它赋值。因此返回的值是不正确的。
|
||||
|
||||
这里有一个坑。对于需要同步的是单个的变量或者内存位置,使用`std::atomic`就足够了。
|
||||
不过,一旦你需要对两个以上的变量或内存位置作为一个单元来操作的话,就应该使用互斥锁。对于`Widget::magicValue`是这样的。
|
||||
这里有一个坑。对于需要同步的是单个的变量或者内存位置,使用`std::atomic`就足够了。不过,一旦你需要对两个以上的变量或内存位置作为一个单元来操作的话,就应该使用互斥量。对于`Widget::magicValue`是这样的。
|
||||
|
||||
```c++
|
||||
class Widget {
|
||||
public:
|
||||
|
||||
…
|
||||
int magicValue() const
|
||||
{
|
||||
std::lock_guard<std::mutex> guard(m); // lock m
|
||||
std::lock_guard<std::mutex> guard(m); //锁定m
|
||||
|
||||
if (cacheValid) return cachedValue;
|
||||
else {
|
||||
auto val1 = expensiveComputation1();
|
||||
@ -174,24 +176,20 @@ public:
|
||||
cacheValid = true;
|
||||
return cachedValue;
|
||||
}
|
||||
} // unlock m
|
||||
} //解锁m
|
||||
…
|
||||
|
||||
private:
|
||||
mutable std::mutex m;
|
||||
mutable int cachedValue; // no longer atomic
|
||||
mutable bool cacheValid{ false }; // no longer atomic
|
||||
mutable int cachedValue; //不再用atomic
|
||||
mutable bool cacheValid{ false }; //不再用atomic
|
||||
};
|
||||
|
||||
```
|
||||
|
||||
现在,这个条款是基于,多个线程可以同时在一个对象上执行一个const成员函数这个假设的。如果你不是在这种情况下编写一个const成员函数。也就是你可以保证在对象上永远不会有多个线程执行该成员函数。再换句话说,该函数的线程安全是无关紧要的。比如,为单线程使用而设计类的成员函数的线程安全是不重要的。在这种情况下你可以避免,因使用 `mutex` 和 `std::atomics`所消耗的资源,以及包含它们的类只能使用移动语义带来的副作用。然而,这种单线程的场景越来越少见,而且很可能会越来越少。可以肯定的是,const成员函数应支持并发执行,这就是为什么你应该确保const成员函数是线程安全的。
|
||||
|
||||
> **应该注意的事情**
|
||||
>
|
||||
> + 确保const成员函数线程安全,除非你确定它们永远不会在临界区(concurrent context)中使用。
|
||||
> + `std::atomic`可能比互斥锁提供更好的性能,但是它只适合操作单个变量或内存位置。
|
||||
|
||||
|
||||
|
||||
现在,这个条款是基于,多个线程可以同时在一个对象上执行一个`const`成员函数这个假设的。如果你不是在这种情况下编写一个`const`成员函数——你可以**保证**在一个对象上永远不会有多个线程执行该成员函数——该函数的线程安全是无关紧要的。比如,为独占单线程使用而设计的类的成员函数是否线程安全并不重要。在这种情况下,你可以避免因使用互斥量和`std::atomics`所消耗的资源,以及包含它们的类只能使用移动语义带来的副作用。然而,这种线程无关的情况越来越少见,而且很可能会越来越少。可以肯定的是,`const`成员函数应支持并发执行,这就是为什么你应该确保`const`成员函数是线程安全的。
|
||||
|
||||
**请记住:**
|
||||
|
||||
+ 确保`const`成员函数线程安全,除非你**确定**它们永远不会在并发上下文(*concurrent context*)中使用。
|
||||
+ 使用`std::atomic`变量可能比互斥量提供更好的性能,但是它只适合操作单个变量或内存位置。
|
||||
|
@ -1,75 +1,82 @@
|
||||
## Item 17:理解特殊成员函数的生成
|
||||
条款 17:理解特殊成员函数函数的生成
|
||||
## 条款十七:理解特殊成员函数的生成
|
||||
**Item 17: Understand special member function generation**
|
||||
|
||||
在C++术语中,特殊成员函数是指C++自己生成的函数。C++98有四个:默认构造函数函数,析构函数,拷贝构造函数,拷贝赋值运算符。这些函数仅在需要的时候才生成,比如某个代码使用它们但是它们没有在类中声明。默认构造函数仅在类完全没有构造函数的时候才生成。(防止编译器为某个类生成构造函数,但是你希望那个构造函数有参数)生成的特殊成员函数是隐式public且inline,除非该类是继承自某个具有虚函数的类,否则生成的析构函数是非虚的。
|
||||
在C++术语中,**特殊成员函数**是指C++自己生成的函数。C++98有四个:默认构造函数,析构函数,拷贝构造函数,拷贝赋值运算符。当然在这里有些细则要注意。这些函数仅在需要的时候才生成,比如某个代码使用它们但是它们没有在类中明确声明。默认构造函数仅在类完全没有构造函数的时候才生成。(防止编译器为某个类生成构造函数,但是你希望那个构造函数有参数)生成的特殊成员函数是隐式public且`inline`,它们是非虚的,除非相关函数是在派生类中的析构函数,派生类继承了有虚析构函数的基类。在这种情况下,编译器为派生类生成的析构函数是虚的。
|
||||
|
||||
但是你早就知道这些了。好吧好吧,都说古老的历史:美索不达米亚,商朝,FORTRAN,C++98。但是时代改变了,C++生成特殊成员的规则也改变了。要留意这些新规则,因为用C++高效编程方面很少有像它们一样重要的东西需要知道。
|
||||
但是你早就知道这些了。好吧好吧,都说古老的历史:美索不达米亚,商朝,FORTRAN,C++98。但是时代改变了,C++生成特殊成员的规则也改变了。要留意这些新规则,知道什么时候编译器会悄悄地向你的类中添加成员函数,因为没有什么比这件事对C++高效编程更重要。
|
||||
|
||||
C++11特殊成员函数俱乐部迎来了两位新会员:移动构造函数和移动赋值运算符。它们的签名是:
|
||||
```cpp
|
||||
class Widget {
|
||||
public:
|
||||
...
|
||||
Widget(Widget&& rhs);
|
||||
Widget& operator=(Widget&& rhs);
|
||||
...
|
||||
…
|
||||
Widget(Widget&& rhs); //移动构造函数
|
||||
Widget& operator=(Widget&& rhs); //移动赋值运算符
|
||||
…
|
||||
};
|
||||
```
|
||||
掌控它们生成和行为的规则类似于拷贝系列。移动操作仅在需要的时候生成,如果生成了,就会对非static数据执行逐成员的移动。那意味着移动构造函数根据`rhs`参数里面对应的成员移动构造出新部分,移动赋值运算符根据参数里面对应的非static成员移动赋值。移动构造函数也移动构造基类部分(如果有的话),移动赋值运算符也是移动赋值基类部分。
|
||||
掌控它们生成和行为的规则类似于拷贝系列。移动操作仅在需要的时候生成,如果生成了,就会对类的non-static数据成员执行逐成员的移动。那意味着移动构造函数根据`rhs`参数里面对应的成员移动构造出新non-static部分,移动赋值运算符根据参数里面对应的non-static成员移动赋值。移动构造函数也移动构造基类部分(如果有的话),移动赋值运算符也是移动赋值基类部分。
|
||||
|
||||
现在,当我对一个数据成员或者基类使用移动构造或者移动赋值时,没有任何保证移动一定会真的发生。逐成员移动,实际上,更像是逐成员移动**请求**,因为对不可移动类型使用移动操作实际上执行的是拷贝操作。逐成员移动的核心是对对象使用**std::move**,然后函数决议时会选择执行移动还是拷贝操作。**Item 23**包括了这个操作的细节。本章中,简单记住如果支持移动就会逐成员移动类成员和基类成员,如果不支持移动就执行拷贝操作就好了。
|
||||
现在,当我对一个数据成员或者基类使用移动构造或者移动赋值时,没有任何保证移动一定会真的发生。逐成员移动,实际上,更像是逐成员移动**请求**,因为对**不可移动类型**(即对移动操作没有特殊支持的类型,比如大部分C++98传统类)使用“移动”操作实际上执行的是拷贝操作。逐成员移动的核心是对对象使用`std::move`,然后函数决议时会选择执行移动还是拷贝操作。[Item23](https://github.com/kelthuzadx/EffectiveModernCppChinese/blob/master/5.RRefMovSemPerfForw/item23.md)包括了这个操作的细节。本条款中,简单记住如果支持移动就会逐成员移动类成员和基类成员,如果不支持移动就执行拷贝操作就好了。
|
||||
|
||||
两个拷贝操作是独立的:声明一个不会限制编译器声明另一个。所以如果你声明一个拷贝构造函数,但是没有声明拷贝赋值运算符,如果写的代码用到了拷贝赋值,编译器会帮助你生成拷贝赋值运算符重载。同样的,如果你声明拷贝赋值运算符但是没有拷贝构造,代码用到拷贝构造编译器就会生成它。上述规则在C++98和C++11中都成立。
|
||||
像拷贝操作情况一样,如果你自己声明了移动操作,编译器就不会生成。然而它们生成的精确条件与拷贝操作的条件有点不同。
|
||||
|
||||
如果你声明了某个移动函数,编译器就不再生成另一个移动函数。这与复制函数的生成规则不太一样:两个复制函数是独立的,声明一个不会影响另一个的默认生成。这条规则的背后原因是,如果你声明了某个移动函数,就表明这个类型的移动操作不再是“逐一移动成员变量”的语义,即你不需要编译器默认生成的移动函数的语义,因此编译器也不会为你生成另一个移动函数。
|
||||
两个拷贝操作是独立的:声明一个不会限制编译器生成另一个。所以如果你声明一个拷贝构造函数,但是没有声明拷贝赋值运算符,如果写的代码用到了拷贝赋值,编译器会帮助你生成拷贝赋值运算符。同样的,如果你声明拷贝赋值运算符但是没有拷贝构造函数,代码用到拷贝构造函数时编译器就会生成它。上述规则在C++98和C++11中都成立。
|
||||
|
||||
再进一步,如果一个类显式声明了拷贝操作,编译器就不会生成移动操作。这种限制的解释是如果声明拷贝操作就暗示着默认逐成员拷贝操作不适用于该类,编译器会明白如果默认拷贝不适用于该类,移动操作也可能是不适用的。
|
||||
两个移动操作不是相互独立的。如果你声明了其中一个,编译器就不再生成另一个。如果你给类声明了,比如,一个移动构造函数,就表明对于移动操作应怎样实现,与编译器应生成的默认逐成员移动有些区别。如果逐成员移动构造有些问题,那么逐成员移动赋值同样也可能有问题。所以声明移动构造函数阻止移动赋值运算符的生成,声明移动赋值运算符同样阻止编译器生成移动构造函数。
|
||||
|
||||
这是另一个方向。声明移动操作使得编译器不会生成拷贝操作。(编译器通过给这些函数加上delete来保证,参见Item11)。毕竟,如果逐成员移动对该类来说不合适,也没有理由指望逐成员拷贝操作是合适的。听起来会破坏C++98的某些代码,因为C++11中拷贝操作可用的条件比C++98更受限,但事实并非如此。C++98的代码没有移动操作,因为C++98中没有移动对象这种概念。只有一种方法能让老代码使用用户声明的移动操作,那就是使用C++11标准然后添加这些操作, 并在享受这些操作带来的好处同时接受C++11特殊成员函数生成规则的限制。
|
||||
再进一步,如果一个类显式声明了拷贝操作,编译器就不会生成移动操作。这种限制的解释是如果声明拷贝操作(构造或者赋值)就暗示着平常拷贝对象的方法(逐成员拷贝)不适用于该类,编译器会明白如果逐成员拷贝对拷贝操作来说不合适,逐成员移动也可能对移动操作来说不合适。
|
||||
|
||||
也许你早已听过_Rule of Three_规则。这个规则告诉我们如果你声明了拷贝构造函数,拷贝赋值运算符,或者析构函数三者之一,你应该也声明其余两个。它来源于长期的观察,即用户接管拷贝操作的需求几乎都是因为该类会做其他资源的管理,这也几乎意味着1)无论哪种资源管理如果能在一个拷贝操作内完成,也应该在另一个拷贝操作内完成2)类析构函数也需要参与资源的管理(通常是释放)。通常意义的资源管理指的是内存(如STL容器会动态管理内存),这也是为什么标准库里面那些管理内存的类都声明了“the big three”:拷贝构造,拷贝赋值和析构。
|
||||
这是另一个方向。声明移动操作(构造或赋值)使得编译器禁用拷贝操作。(编译器通过给拷贝操作加上*delete*来保证,参见[Item11](https://github.com/kelthuzadx/EffectiveModernCppChinese/blob/master/3.MovingToModernCpp/item11.md)。)(译注:禁用的是自动生成的拷贝操作,对于用户声明的拷贝操作不受影响)毕竟,如果逐成员移动对该类来说不合适,也没有理由指望逐成员拷贝操作是合适的。听起来会破坏C++98的某些代码,因为C++11中拷贝操作可用的条件比C++98更受限,但事实并非如此。C++98的代码没有移动操作,因为C++98中没有移动对象这种概念。只有一种方法能让老代码使用用户声明的移动操作,那就是使用C++11标准然后添加这些操作,使用了移动语义的类必须接受C++11特殊成员函数生成规则的限制。
|
||||
|
||||
**Rule of Three**带来的后果就是只要出现用户定义的析构函数就意味着简单的逐成员拷贝操作不适用于该类。接着,如果一个类声明了析构也意味着拷贝操作可能不应该自定生成,因为它们做的事情可能是错误的。在C++98提出的时候,上述推理没有得倒足够的重视,所以C++98用户声明析构不会左右编译器生成拷贝操作的意愿。C++11中情况仍然如此,但仅仅是因为限制拷贝操作生成的条件会破坏老代码。
|
||||
也许你早已听过_Rule of Three_规则。这个规则告诉我们如果你声明了拷贝构造函数,拷贝赋值运算符,或者析构函数三者之一,你应该也声明其余两个。它来源于长期的观察,即用户接管拷贝操作的需求几乎都是因为该类会做其他资源的管理,这也几乎意味着(1)无论哪种资源管理如果在一个拷贝操作内完成,也应该在另一个拷贝操作内完成(2)类的析构函数也需要参与资源的管理(通常是释放)。通常要管理的资源是内存,这也是为什么标准库里面那些管理内存的类(如会动态内存管理的STL容器)都声明了“*the big three*”:拷贝构造,拷贝赋值和析构。
|
||||
|
||||
**Rule of Three**规则背后的解释依然有效,再加上对声明拷贝操作阻止移动操作隐式生成的观察,使得C++11不会为那些有用户定义的析构函数的类生成移动操作。所以仅当下面条件成立时才会生成移动操作:
|
||||
*Rule of Three*带来的后果就是只要出现用户定义的析构函数就意味着简单的逐成员拷贝操作不适用于该类。那意味着如果一个类声明了析构,拷贝操作可能不应该自动生成,因为它们做的事情可能是错误的。在C++98提出的时候,上述推理没有得倒足够的重视,所以C++98用户声明析构函数不会左右编译器生成拷贝操作的意愿。C++11中情况仍然如此,但仅仅是因为限制拷贝操作生成的条件会破坏老代码。
|
||||
|
||||
*Rule of Three*规则背后的解释依然有效,再加上对声明拷贝操作阻止移动操作隐式生成的观察,使得C++11不会为那些有用户定义的析构函数的类生成移动操作。
|
||||
|
||||
所以仅当下面条件成立时才会生成移动操作(当需要时):
|
||||
|
||||
+ 类中没有拷贝操作
|
||||
+ 类中没有移动操作
|
||||
+ 类中没有用户定义的析构
|
||||
|
||||
有时,类似的规则也会扩展至移动操作上面,因为现在类声明了拷贝操作,C++11不会为它们自动生成其他拷贝操作。这意味着如果你的某个声明了析构或者拷贝的类依赖自动生成的拷贝操作,你应该考虑升级这些类,消除依赖。假设编译器生成的函数行为是正确的(即逐成员拷贝类数据是你期望的行为),你的工作很简单,C++11的`=default`就可以表达你想做的:
|
||||
有时,类似的规则也会扩展至拷贝操作上面,C++11抛弃了已声明拷贝操作或析构函数的类的拷贝操作的自动生成。这意味着如果你的某个声明了析构或者拷贝的类依赖自动生成的拷贝操作,你应该考虑升级这些类,消除依赖。假设编译器生成的函数行为是正确的(即逐成员拷贝类non-static数据是你期望的行为),你的工作很简单,C++11的`= default`就可以表达你想做的:
|
||||
```cpp
|
||||
class Widget {
|
||||
public:
|
||||
...
|
||||
~Widget();
|
||||
...
|
||||
Widget(const Widget&) = default;
|
||||
Widget&
|
||||
operator=(const Widget&) = default; // behavior is OK
|
||||
...
|
||||
public:
|
||||
…
|
||||
~Widget(); //用户声明的析构函数
|
||||
… //默认拷贝构造函数
|
||||
Widget(const Widget&) = default; //的行为还可以
|
||||
|
||||
Widget& //默认拷贝赋值运算符
|
||||
operator=(const Widget&) = default; //的行为还可以
|
||||
…
|
||||
};
|
||||
```
|
||||
这种方法通常在多态基类中很有用,即根据继承自哪个类来定义接口。多态基类通常有一个虚析构函数,因为如果它们非虚,一些操作(比如对一个基类指针或者引用使用delete或者typeid)会产生未定义或错误结果。除非类继承自一个已经是virtual的析构函数,否则要想析构为虚函数的唯一方法就是加上virtual关键字。通常,默认实现是对的,`=default`是一个不错的方式表达默认实现。然而用户声明的析构函数会抑制编译器生成移动操作,所以如果该类需要具有移动性,就为移动操作加上`=default`。声明移动会抑制拷贝生成,所以如果拷贝性也需要支持,再为拷贝操作加上`=default`:
|
||||
这种方法通常在多态基类中很有用,即通过操作的是哪个派生类对象来定义接口。多态基类通常有一个虚析构函数,因为如果它们非虚,一些操作(比如通过一个基类指针或者引用对派生类对象使用`delete`或者`typeid`)会产生未定义或错误结果。除非类继承了一个已经是*virtual*的析构函数,否则要想析构函数为虚函数的唯一方法就是加上`virtual`关键字。通常,默认实现是对的,`= default`是一个不错的方式表达默认实现。然而用户声明的析构函数会抑制编译器生成移动操作,所以如果该类需要具有移动性,就为移动操作加上`= default`。声明移动会抑制拷贝生成,所以如果拷贝性也需要支持,再为拷贝操作加上`= default`:
|
||||
```cpp
|
||||
class Base {
|
||||
public:
|
||||
virtual ~Base() = default;
|
||||
Base(Base&&) = default;
|
||||
Base& operator=(Base&&) = default;
|
||||
Base(const Base&) = default;
|
||||
Base& operator=(const Base&) = default;
|
||||
...
|
||||
virtual ~Base() = default; //使析构函数virtual
|
||||
|
||||
Base(Base&&) = default; //支持移动
|
||||
Base& operator=(Base&&) = default;
|
||||
|
||||
Base(const Base&) = default; //支持拷贝
|
||||
Base& operator=(const Base&) = default;
|
||||
…
|
||||
};
|
||||
```
|
||||
实际上,就算编译器乐于为你的类生成拷贝和移动操作,生成的函数也如你所愿,你也应该手动声明它们然后加上`=default`。这看起来比较多余,但是它让你的意图更明确,也能帮助你避免一些微妙的bug。比如,你有一个字符串哈希表,即键为整数id,值为字符串,支持快速查找的数据结构:
|
||||
实际上,就算编译器乐于为你的类生成拷贝和移动操作,生成的函数也如你所愿,你也应该手动声明它们然后加上`= default`。这看起来比较多余,但是它让你的意图更明确,也能帮助你避免一些微妙的bug。比如,你有一个类来表示字符串表,即一种支持使用整数ID快速查找字符串值的数据结构:
|
||||
```cpp
|
||||
class StringTable {
|
||||
public:
|
||||
StringTable() {}
|
||||
...
|
||||
private:
|
||||
std::map<int, std::string> values;
|
||||
class StringTable {
|
||||
public:
|
||||
StringTable() {}
|
||||
… //插入、删除、查找等函数,但是没有拷贝/移动/析构功能
|
||||
private:
|
||||
std::map<int, std::string> values;
|
||||
};
|
||||
```
|
||||
假设这个类没有声明拷贝操作,没有移动操作,也没有析构,如果它们被用到编译器会自动生成。没错,很方便。
|
||||
@ -78,43 +85,45 @@ public:
|
||||
```cpp
|
||||
class StringTable {
|
||||
public:
|
||||
StringTable()
|
||||
{ makeLogEntry("Creating StringTable object"); }
|
||||
StringTable()
|
||||
{ makeLogEntry("Creating StringTable object"); } //增加的
|
||||
|
||||
~StringTable()
|
||||
{ makeLogEntry("Destroying StringTable object"); }
|
||||
...
|
||||
Item 17 | 113
|
||||
~StringTable() //也是增加的
|
||||
{ makeLogEntry("Destroying StringTable object"); }
|
||||
… //其他函数同之前一样
|
||||
private:
|
||||
std::map<int, std::string> values; // as before
|
||||
std::map<int, std::string> values; //同之前一样
|
||||
};
|
||||
```
|
||||
看起来合情合理,但是声明析构有潜在的副作用:它阻止了移动操作的生成。然而,拷贝操作的生成是不受影响的。因此代码能通过编译,运行,也能通过功能(译注:即打日志的功能)测试。功能测试也包括移动功能,因为即使该类不支持移动操作,对该类的移动请求也能通过编译和运行。这个请求正如之前提到的,会转而由拷贝操作完成。它因为着对**StringTable**对象的移动实际上是对对象的拷贝,即拷贝里面的`std::map<int, std::string>`对象。拷贝`std::map<int, std::string>`对象很可能比移动慢几个数量级。简单的加个析构就引入了极大的性能问题!对拷贝和移动操作显式加个`=default`,问题将不再出现。
|
||||
看起来合情合理,但是声明析构有潜在的副作用:它阻止了移动操作的生成。然而,拷贝操作的生成是不受影响的。因此代码能通过编译,运行,也能通过功能(译注:即打日志的功能)测试。功能测试也包括移动功能,因为即使该类不支持移动操作,对该类的移动请求也能通过编译和运行。这个请求正如之前提到的,会转而由拷贝操作完成。它意味着对`StringTable`对象的移动实际上是对对象的拷贝,即拷贝里面的`std::map<int, std::string>`对象。拷贝`std::map<int, std::string>`对象很可能比移动慢**几个数量级**。简单的加个析构就引入了极大的性能问题!对拷贝和移动操作显式加个`= default`,问题将不再出现。
|
||||
|
||||
受够了我喋喋不休的讲述C++11拷贝移动规则了吧,你可能想知道什么时候我才会把注意力转入到剩下两个特殊成员函数,默认构造和析构。现在就是时候了,但是只有一句话,因为它们几乎没有改变:它们在C++98中是什么样,在C++11中就是什么样。
|
||||
受够了我喋喋不休的讲述C++11拷贝移动规则了吧,你可能想知道什么时候我才会把注意力转入到剩下两个特殊成员函数,默认构造函数和析构函数。现在就是时候了,但是只有一句话,因为它们几乎没有改变:它们在C++98中是什么样,在C++11中就是什么样。
|
||||
|
||||
C++11对于特殊成员函数处理的规则如下:
|
||||
|
||||
+ 默认构造函数:和C++98规则相同。仅当类不存在用户声明的构造函数时才自动生成。
|
||||
+ 析构函数:基本上和C++98相同;稍微不同的是现在析构默认**noexcept**(参见Item14)。和C++98一样,仅当基类析构为虚函数时该类析构才为虚函数。
|
||||
+ 拷贝构造函数:和C++98运行时行为一样:逐成员拷贝非static数据。仅当类没有用户定义的拷贝构造时才生成。如果类声明了移动操作它就是**delete**。当用户声明了拷贝赋值或者析构,该函数不再自动生成。
|
||||
+ 拷贝赋值运算符:和C++98运行时行为一样:逐成员拷贝赋值非static数据。仅当类没有用户定义的拷贝赋值时才生成。如果类声明了移动操作它就是**delete**。当用户声明了拷贝构造或者析构,该函数不再自动生成。
|
||||
+ 移动构造函数和移动赋值运算符:都对非static数据执行逐成员移动。仅当类没有用户定义的拷贝操作,移动操作或析构时才自动生成。
|
||||
+ **默认构造函数**:和C++98规则相同。仅当类不存在用户声明的构造函数时才自动生成。
|
||||
+ **析构函数**:基本上和C++98相同;稍微不同的是现在析构默认`noexcept`(参见[Item14](https://github.com/kelthuzadx/EffectiveModernCppChinese/blob/master/3.MovingToModernCpp/item14.md))。和C++98一样,仅当基类析构为虚函数时该类析构才为虚函数。
|
||||
+ **拷贝构造函数**:和C++98运行时行为一样:逐成员拷贝non-static数据。仅当类没有用户定义的拷贝构造时才生成。如果类声明了移动操作它就是*delete*的。当用户声明了拷贝赋值或者析构,该函数自动生成已被废弃。
|
||||
+ **拷贝赋值运算符**:和C++98运行时行为一样:逐成员拷贝赋值non-static数据。仅当类没有用户定义的拷贝赋值时才生成。如果类声明了移动操作它就是*delete*的。当用户声明了拷贝构造或者析构,该函数自动生成已被废弃。
|
||||
+ **移动构造函数**和**移动赋值运算符**:都对非static数据执行逐成员移动。仅当类没有用户定义的拷贝操作,移动操作或析构时才自动生成。
|
||||
|
||||
注意没有成员函数模版阻止编译器生成特殊成员函数的规则。这意味着如果**Widget**是这样:
|
||||
注意没有“成员函数**模版**阻止编译器生成特殊成员函数”的规则。这意味着如果`Widget`是这样:
|
||||
```cpp
|
||||
class Widget {
|
||||
...
|
||||
template<typename T>
|
||||
Widget(const T& rhs);
|
||||
…
|
||||
template<typename T> //从任何东西构造Widget
|
||||
Widget(const T& rhs);
|
||||
|
||||
template<typename T>
|
||||
Widget& operator=(const T& rhs); ...
|
||||
template<typename T> //从任何东西赋值给Widget
|
||||
Widget& operator=(const T& rhs);
|
||||
…
|
||||
};
|
||||
```
|
||||
编译器仍会生成移动和拷贝操作(假设正常生成它们的条件满足),即使可以模板实例化产出拷贝构造和拷贝赋值运算符的函数签名。(当T为Widget时)。很可能你会决定这是一个不值得承认的边缘情况,但是我提到它是有道理的,Item26将会详细讨论它可能带来的后果。
|
||||
编译器仍会生成移动和拷贝操作(假设正常生成它们的条件满足),即使可以模板实例化产出拷贝构造和拷贝赋值运算符的函数签名。(当`T`为`Widget`时。)很可能你会觉得这是一个不值得承认的边缘情况,但是我提到它是有道理的,[Item26](https://github.com/kelthuzadx/EffectiveModernCppChinese/blob/master/5.RRefMovSemPerfForw/item26.md)将会详细讨论它可能带来的后果。
|
||||
|
||||
**记住**:
|
||||
+ 特殊成员函数是编译器可能自动生成的函数:默认构造,析构,拷贝操作,移动操作。
|
||||
+ 移动操作仅当类没有显式声明移动操作,拷贝操作,析构时才自动生成。
|
||||
+ 拷贝构造仅当类没有显式声明拷贝构造时才自动生成,并且如果用户声明了移动操作,拷贝构造就是delete。拷贝赋值运算符仅当类没有显式声明拷贝赋值运算符时才自动生成,并且如果用户声明了移动操作,拷贝赋值运算符就是delete。当用户声明了析构函数,拷贝操作不再自动生成。
|
||||
**请记住:**
|
||||
|
||||
+ 特殊成员函数是编译器可能自动生成的函数:默认构造函数,析构函数,拷贝操作,移动操作。
|
||||
+ 移动操作仅当类没有显式声明移动操作,拷贝操作,析构函数时才自动生成。
|
||||
+ 拷贝构造函数仅当类没有显式声明拷贝构造函数时才自动生成,并且如果用户声明了移动操作,拷贝构造就是*delete*。拷贝赋值运算符仅当类没有显式声明拷贝赋值运算符时才自动生成,并且如果用户声明了移动操作,拷贝赋值运算符就是*delete*。当用户声明了析构函数,拷贝操作的自动生成已被废弃。
|
||||
+ 成员函数模板不抑制特殊成员函数的生成。
|
||||
|
@ -1,53 +1,52 @@
|
||||
# CHAPTER 4 Smart Pointers
|
||||
# 第4章 智能指针
|
||||
|
||||
诗人和歌曲作家喜欢爱。有时候喜欢计数。很少情况下两者兼有。受伊丽莎白·巴雷特·勃朗宁(Elizabeth Barrett Browning)对爱和数的不同看法的启发(“我怎么爱你?”让我数一数。”)和保罗·西蒙(Paul Simon)(“离开你的爱人必须有50种方法。”),我们可以试着枚举一些为什么原始指针很难被爱的原因:
|
||||
**CHAPTER 4 Smart Pointers**
|
||||
|
||||
诗人和歌曲作家喜欢爱。有时候喜欢计数。很少情况下两者兼有。受伊丽莎白·巴雷特·勃朗宁(Elizabeth Barrett Browning)对爱和数的不同看法的启发(“我怎么爱你?让我数一数。”)和保罗·西蒙(Paul Simon)(“离开你的爱人必须有50种方法。”),我们可以试着枚举一些为什么原始指针很难被爱的原因:
|
||||
|
||||
1. 它的声明不能指示所指到底是单个对象还是数组。
|
||||
2. 它的声明没有告诉你用完后是否应该销毁它,即指针是否拥有所指之物。
|
||||
3. 如果你决定你应该销毁对象所指,没人告诉你该用delete还是其他析构机制(比如将指针传给专门的销毁函数)。
|
||||
4. 如果你发现该用delete。 原因1说了不知道是delete单个对象还是delete数组。如果用错了结果是未定义的。
|
||||
5. 假设你确定了指针所指,知道销毁机制,也很难确定你在所有执行路径上都执行了销毁操作(包括异常产生后的路径)。少一条路径就会产生资源泄漏,销毁多次还会导致未定义行为。
|
||||
6. 一般来说没有办法告诉你指针是否变成了悬空指针(dangling pointers),即内存中不再存在指针所指之物。悬空指针会在对象销毁后仍然指向它们。
|
||||
3. 如果你决定你应该销毁指针所指对象,没人告诉你该用`delete`还是其他析构机制(比如将指针传给专门的销毁函数)。
|
||||
4. 如果你发现该用`delete`。 原因1说了可能不知道该用单个对象形式(“`delete`”)还是数组形式(“`delete[]`”)。如果用错了结果是未定义的。
|
||||
5. 假设你确定了指针所指,知道销毁机制,也很难确定你在所有执行路径上都执行了**恰为一次**销毁操作(包括异常产生后的路径)。少一条路径就会产生资源泄漏,销毁多次还会导致未定义行为。
|
||||
6. 一般来说没有办法告诉你指针是否变成了悬空指针(dangling pointers),即内存中不再存在指针所指之物。在对象销毁后指针仍指向它们就会产生悬空指针。
|
||||
|
||||
原始指针是强大的工具,当然,另一方面几十年的经验证明,只要注意力稍有疏忽,这个强大的工具就会攻击它的主人。
|
||||
|
||||
智能指针是解决这些问题的一种办法。智能指针包裹原始指针,它们的行为看起来像被包裹的原始指针,但避免了原始指针的很多陷阱。你应该更倾向于智能指针而不是原始指针。几乎原始指针能做的所有事情智能指针都能做,而且出错的机会更少。
|
||||
**智能指针**(*smart 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`放入容器)。
|
||||
`std::auto_ptr`是来自C++98的已废弃遗留物,它是一次标准化的尝试,后来变成了C++11的`std::unique_ptr`。要正确的模拟原生指针需要移动语义,但是C++98没有这个东西。取而代之,`std::auto_ptr`拉拢拷贝操作来达到自己的移动意图。这导致了令人奇怪的代码(拷贝一个`std::auto_ptr`会将它本身设置为null!)和令人沮丧的使用限制(比如不能将`std::auto_ptr`放入容器)。
|
||||
|
||||
`std::unique_ptr`能做`std::auto_ptr`可以做的所有事情以及更多。它能高效完成任务,而且不会扭曲拷贝语义。在所有方面它都比`std::unique_ptr`好。现在`std::auto_ptr`唯一合法的使用场景就是代码使用C++98编译器编译。除非你有上述限制,否则你就该把`std::auto_ptr`替换为`std::unique_ptr`而且绝不回头。
|
||||
`std::unique_ptr`能做`std::auto_ptr`可以做的所有事情以及更多。它能高效完成任务,而且不会扭曲自己的原本含义而变成拷贝对象。在所有方面它都比`std::auto_ptr`好。现在`std::auto_ptr`唯一合法的使用场景就是代码使用C++98编译器编译。除非你有上述限制,否则你就该把`std::auto_ptr`替换为`std::unique_ptr`而且绝不回头。
|
||||
|
||||
各种智能指针的API有极大的不同。唯一功能性相似的可能就是默认构造函数。因为有很多关于这些API的详细手册,所以我将只关注那些API概览没有提及的内容,比如值得注意的使用场景,运行时性能分析等,掌握这些信息可以更高效的使用智能指针。
|
||||
|
||||
|
||||
## Item 18:对于独占资源使用std::unique_ptr
|
||||
当你需要一个智能指针时,`std::unique_ptr`通常是最合适的。可以合理假设,默认情况下,`std::unique_ptr`等同于原始指针,而且对于大多数操作(包括取消引用),他们执行的指令完全相同。这意味着你甚至可以在内存和时间都比较紧张的情况下使用它。如果原始指针够小够快,那么`std::unique_ptr`一样可以。
|
||||
## 条款十八:对于独占资源使用`std::unique_ptr`
|
||||
**Item 18: Use `std::unique_ptr` for exclusive-ownership resource management**
|
||||
|
||||
`std::unique_ptr`体现了专有所有权语义。一个`non-null std::unique_ptr`始终有其指向的内容。移动操作将所有权从源指针转移到目的指针,拷贝操作是不允许的,因为如果你能拷贝一个`std::unique_ptr`,你会得到指向相同内容的两个`std::unique_ptr`,每个都认为自己拥有资源,销毁时就会出现重复销毁。因此,`std::unique_ptr`只支持移动操作。当`std::unique_ptr`销毁时,其指向的资源也执行析构函数。而原始指针需要显示调用delete来销毁指针指向的资源。
|
||||
当你需要一个智能指针时,`std::unique_ptr`通常是最合适的。可以合理假设,默认情况下,`std::unique_ptr`大小等同于原始指针,而且对于大多数操作(包括取消引用),他们执行的指令完全相同。这意味着你甚至可以在内存和时间都比较紧张的情况下使用它。如果原始指针够小够快,那么`std::unique_ptr`一样可以。
|
||||
|
||||
`std::unique_ptr`的常见用法是作为继承层次结构中对象的工厂函数返回类型。假设我们有一个基类Investment(比如 stocks,bonds,real estate等)的继承结构。
|
||||
`std::unique_ptr`体现了专有所有权(*exclusive ownership*)语义。一个non-null `std::unique_ptr`始终拥有其指向的内容。移动一个`std::unique_ptr`将所有权从源指针转移到目的指针。(源指针被设为null。)拷贝一个`std::unique_ptr`是不允许的,因为如果你能拷贝一个`std::unique_ptr`,你会得到指向相同内容的两个`std::unique_ptr`,每个都认为自己拥有(并且应当最后销毁)资源,销毁时就会出现重复销毁。因此,`std::unique_ptr`是一种只可移动类型(*move-only type*)。当析构时,一个non-null `std::unique_ptr`销毁它指向的资源。默认情况下,资源析构通过对`std::unique_ptr`里原始指针调用`delete`来实现。
|
||||
|
||||
`std::unique_ptr`的常见用法是作为继承层次结构中对象的工厂函数返回类型。假设我们有一个投资类型(比如股票、债券、房地产等)的继承结构,使用基类`Investment`。
|
||||
|
||||
```cpp
|
||||
class Investment { ... };
|
||||
class Stock: public Investment {...};
|
||||
class Bond: public Investment {...};
|
||||
class RealEstate: public Investment {...};
|
||||
class Investment { … };
|
||||
class Stock: public Investment { … };
|
||||
class Bond: public Investment { … };
|
||||
class RealEstate: public Investment { … };
|
||||
```
|
||||
|
||||
```mermaid
|
||||
classDiagram
|
||||
Investment <|-- Stock
|
||||
Investment <|-- Bond
|
||||
Investment <|-- RealEstate
|
||||
```
|
||||

|
||||
|
||||
这种继承关系的工厂函数在堆上分配一个对象然后返回指针,调用方在不需要的时候,销毁对象。这使用场景完美匹配`std::unique_ptr`,因为调用者对工厂返回的资源负责(即对该资源的专有所有权),并且`std::unique_ptr`会自动销毁指向的内容。可以这样声明:
|
||||
这种继承关系的工厂函数在堆上分配一个对象然后返回指针,调用方在不需要的时候有责任销毁对象。这使用场景完美匹配`std::unique_ptr`,因为调用者对工厂返回的资源负责(即对该资源的专有所有权),并且`std::unique_ptr`在自己被销毁时会自动销毁指向的内容。`Investment`继承关系的工厂函数可以这样声明:
|
||||
|
||||
```cpp
|
||||
template<typename... Ts>
|
||||
std::unique_ptr<Investment>
|
||||
template<typename... Ts> //返回指向对象的std::unique_ptr,
|
||||
std::unique_ptr<Investment> //对象使用给定实参创建
|
||||
makeInvestment(Ts&&... params);
|
||||
```
|
||||
|
||||
@ -55,136 +54,143 @@ makeInvestment(Ts&&... params);
|
||||
|
||||
```cpp
|
||||
{
|
||||
...
|
||||
auto pInvestment = makeInvestment(arguments);
|
||||
...
|
||||
} //destroy *pInvestment
|
||||
…
|
||||
auto pInvestment = //pInvestment是
|
||||
makeInvestment( arguments ); //std::unique_ptr<Investment>类型
|
||||
…
|
||||
} //销毁 *pInvestment
|
||||
```
|
||||
|
||||
但是也可以在所有权转移的场景中使用它,比如将工厂返回的`std::unique_ptr`移入容器中,然后将容器元素移入对象的数据成员中,然后对象随即被销毁。发生这种情况时,并且销毁该对象将导致销毁从工厂返回的资源,对象`std::unique_ptr`的数据成员也被销毁。如果所有权链由于异常或者其他非典型控制流出现中断(比如提前return函数或者循环中的break),则拥有托管资源的`std::unique_ptr`将保证指向内容的析构函数被调用,销毁对应资源。
|
||||
但是也可以在所有权转移的场景中使用它,比如将工厂返回的`std::unique_ptr`移入容器中,然后将容器元素移入一个对象的数据成员中,然后对象过后被销毁。发生这种情况时,这个对象的`std::unique_ptr`数据成员也被销毁,并且智能指针数据成员的析构将导致从工厂返回的资源被销毁。如果所有权链由于异常或者其他非典型控制流出现中断(比如提前从函数return或者循环中的`break`),则拥有托管资源的`std::unique_ptr`将保证指向内容的析构函数被调用,销毁对应资源。(这个规则也有些例外。大多数情况发生于不正常的程序终止。如果一个异常传播到线程的基本函数(比如程序初始线程的`main`函数)外,或者违反`noexcept`说明(见[Item14](https://github.com/kelthuzadx/EffectiveModernCppChinese/blob/master/3.MovingToModernCpp/item14.md)),局部变量可能不会被销毁;如果`std::abort`或者退出函数(如`std::_Exit`,`std::exit`,或`std::quick_exit`)被调用,局部变量一定没被销毁。)
|
||||
|
||||
默认情况下,销毁将通过delete进行,但是在构造过程中,可以自定义`std::unique_ptr`指向对象的析构函数:任意函数(或者函数对象,包括lambda)。如果通过`makeInvestment`创建的对象不能直接被删除,应该首先写一条日志,可以实现如下:
|
||||
默认情况下,销毁将通过`delete`进行,但是在构造过程中,`std::unique_ptr`对象可以被设置为使用(对资源的)**自定义删除器**:当资源需要销毁时可调用的任意函数(或者函数对象,包括*lambda*表达式)。如果通过`makeInvestment`创建的对象不应仅仅被`delete`,而应该先写一条日志,`makeInvestment`可以以如下方式实现。(代码后有说明,别担心有些东西的动机不那么明显。)
|
||||
|
||||
```cpp
|
||||
auto delInvmt = [](Investment* pInvestment)
|
||||
{
|
||||
makeLogEntry(pInvestment);
|
||||
delete pInvestment;
|
||||
};
|
||||
auto delInvmt = [](Investment* pInvestment) //自定义删除器
|
||||
{ //(lambda表达式)
|
||||
makeLogEntry(pInvestment);
|
||||
delete pInvestment;
|
||||
};
|
||||
|
||||
template<typename... Ts>
|
||||
std::unique_ptr<Investment, decltype(delInvmt)>
|
||||
makeInvestment(Ts&& params)
|
||||
std::unique_ptr<Investment, decltype(delInvmt)> //更改后的返回类型
|
||||
makeInvestment(Ts&&... params)
|
||||
{
|
||||
std::unique_ptr<Investment, decltype(delInvmt)> pInv(nullptr, delInvmt);
|
||||
if (/*a Stock object should be created*/)
|
||||
{
|
||||
pInv.reset(new Stock(std::forward<Ts>(params)...));
|
||||
}
|
||||
else if ( /* a Bond object should be created */ )
|
||||
{
|
||||
pInv.reset(new Bond(std::forward<Ts>(params)...));
|
||||
}
|
||||
else if ( /* a RealEstate object should be created */ )
|
||||
{
|
||||
pInv.reset(new RealEstate(std::forward<Ts>(params)...));
|
||||
}
|
||||
return pInv;
|
||||
std::unique_ptr<Investment, decltype(delInvmt)> //应返回的指针
|
||||
pInv(nullptr, delInvmt);
|
||||
if (/*一个Stock对象应被创建*/)
|
||||
{
|
||||
pInv.reset(new Stock(std::forward<Ts>(params)...));
|
||||
}
|
||||
else if ( /*一个Bond对象应被创建*/ )
|
||||
{
|
||||
pInv.reset(new Bond(std::forward<Ts>(params)...));
|
||||
}
|
||||
else if ( /*一个RealEstate对象应被创建*/ )
|
||||
{
|
||||
pInv.reset(new RealEstate(std::forward<Ts>(params)...));
|
||||
}
|
||||
return pInv;
|
||||
}
|
||||
```
|
||||
|
||||
稍后,我将解释其工作原理,但首先请考虑如果你是调用者,情况如何。假设你存储`makeInvestment`调用结果在auto变量中,那么你将在愉快中忽略在删除过程中需要特殊处理的事实,当然,你确实幸福,因为使用了`unique_ptr`意味着你不需要考虑在资源释放时的路径,以及确保只释放一次,`std::unique_ptr`自动解决了这些问题。从使用者角度,`makeInvestment`接口很棒。
|
||||
稍后,我将解释其工作原理,但首先请考虑如果你是调用者,情况如何。假设你存储`makeInvestment`调用结果到`auto`变量中,那么你将在愉快中忽略在删除过程中需要特殊处理的事实。当然,你确实幸福,因为使用了`unique_ptr`意味着你不需要关心什么时候资源应被释放,不需要考虑在资源释放时的路径,以及确保只释放一次,`std::unique_ptr`自动解决了这些问题。从使用者角度,`makeInvestment`接口很棒。
|
||||
|
||||
这个实现确实相当棒,如果你理解了:
|
||||
|
||||
- `delInvmt`是自定义的从`makeInvestment`返回的析构函数。所有的自定义的析构行为接受要销毁对象的原始指针,然后执行销毁操作。如上例子。使用lambda创建`delInvmt`是方便的,而且,正如稍后看到的,比编写常规的函数更有效
|
||||
- `delInvmt`是从`makeInvestment`返回的对象的自定义的删除器。所有的自定义的删除行为接受要销毁对象的原始指针,然后执行所有必要行为实现销毁操作。在上面情况中,操作包括调用`makeLogEntry`然后应用`delete`。使用*lambda*创建`delInvmt`是方便的,而且,正如稍后看到的,比编写常规的函数更有效。
|
||||
|
||||
- 当使用自定义删除器时,必须将其作为第二个参数传给`std::unique_ptr`。对于decltype,更多信息查看Item3
|
||||
- 当使用自定义删除器时,删除器类型必须作为第二个类型实参传给`std::unique_ptr`。在上面情况中,就是`delInvmt`的类型,这就是为什么`makeInvestment`返回类型是`std::unique_ptr<Investment, decltype(delInvmt)>`。(对于`decltype`,更多信息查看[Item3](https://github.com/kelthuzadx/EffectiveModernCppChinese/blob/master/1.DeducingTypes/item3.md))
|
||||
|
||||
- `makeInvestment`的基本策略是创建一个空的`std::unique_ptr`,然后指向一个合适类型的对象,然后返回。为了与pInv关联自定义删除器,作为构造函数的第二个参数
|
||||
- `makeInvestment`的基本策略是创建一个空的`std::unique_ptr`,然后指向一个合适类型的对象,然后返回。为了将自定义删除器`delInvmt`与`pInv`关联,我们把`delInvmt`作为`pInv`构造函数的第二个实参。
|
||||
|
||||
- 尝试将原始指针(比如new创建)赋值给`std::unique_ptr`通不过编译,因为不存在从原始指针到智能指针的隐式转换。这种隐式转换会出问题,所以禁止。这就是为什么通过`reset`来传递new指针的原因
|
||||
- 尝试将原始指针(比如`new`创建)赋值给`std::unique_ptr`通不过编译,因为是一种从原始指针到智能指针的隐式转换。这种隐式转换会出问题,所以C++11的智能指针禁止这个行为。这就是通过`reset`来让`pInv`接管通过`new`创建的对象的所有权的原因。
|
||||
|
||||
- 使用new时,要使用`std::forward`作为参数来完美转发给`makeInvestment`(查看Item 25)。这使调用者提供的所有信息可用于正在创建的对象的构造函数
|
||||
- 使用`new`时,我们使用`std::forward`把传给`makeInvestment`的实参完美转发出去(查看[Item25](https://github.com/kelthuzadx/EffectiveModernCppChinese/blob/master/5.RRefMovSemPerfForw/item25.md))。这使调用者提供的所有信息可用于正在创建的对象的构造函数。
|
||||
|
||||
- 自定义删除器的参数类型是`Investment*`,尽管真实的对象类型是在`makeInvestment`内部创建的,它最终通过在lambda表达式中,作为`Investment*`对象被删除。这意味着我们通过基类指针删除派生类实例,为此,基类必须是虚函数析构:
|
||||
- 自定义删除器的一个形参,类型是`Investment*`,不管在`makeInvestment`内部创建的对象的真实类型(如`Stock`,`Bond`,或`RealEstate`)是什么,它最终在*lambda*表达式中,作为`Investment*`对象被删除。这意味着我们通过基类指针删除派生类实例,为此,基类`Investment`必须有虚析构函数:
|
||||
|
||||
```cpp
|
||||
class Investment {
|
||||
public:
|
||||
...
|
||||
virtual ~Investment();
|
||||
...
|
||||
public:
|
||||
…
|
||||
virtual ~Investment(); //关键设计部分!
|
||||
…
|
||||
};
|
||||
```
|
||||
|
||||
在C++14中,函数的返回类型推导存在(参阅Item 3),意味着`makeInvestment`可以更简单,封装的方式实现:
|
||||
在C++14中,函数的返回类型推导存在(参阅[Item3](https://github.com/kelthuzadx/EffectiveModernCppChinese/blob/master/1.DeducingTypes/item3.md)),意味着`makeInvestment`可以以更简单,更封装的方式实现:
|
||||
|
||||
```cpp
|
||||
template<typename... Ts>
|
||||
auto makeInvestment(Ts&& params)
|
||||
auto makeInvestment(Ts&&... params) //C++14
|
||||
{
|
||||
auto delInvmt = [](Investment* pInvestment)
|
||||
{
|
||||
makeLogEntry(pInvestment);
|
||||
delete pInvestment;
|
||||
};
|
||||
std::unique_ptr<Investment, decltype(delInvmt)> pInv(nullptr, delInvmt);
|
||||
if (/*a Stock object should be created*/)
|
||||
{
|
||||
pInv.reset(new Stock(std::forward<Ts>(params)...));
|
||||
}
|
||||
else if ( /* a Bond object should be created */ )
|
||||
{
|
||||
pInv.reset(new Bond(std::forward<Ts>(params)...));
|
||||
}
|
||||
else if ( /* a RealEstate object should be created */ )
|
||||
{
|
||||
pInv.reset(new RealEstate(std::forward<Ts>(params)...));
|
||||
}
|
||||
return pInv;
|
||||
auto delInvmt = [](Investment* pInvestment) //现在在
|
||||
{ //makeInvestment里
|
||||
makeLogEntry(pInvestment);
|
||||
delete pInvestment;
|
||||
};
|
||||
|
||||
std::unique_ptr<Investment, decltype(delInvmt)> //同之前一样
|
||||
pInv(nullptr, delInvmt);
|
||||
if ( … ) //同之前一样
|
||||
{
|
||||
pInv.reset(new Stock(std::forward<Ts>(params)...));
|
||||
}
|
||||
else if ( … ) //同之前一样
|
||||
{
|
||||
pInv.reset(new Bond(std::forward<Ts>(params)...));
|
||||
}
|
||||
else if ( … ) //同之前一样
|
||||
{
|
||||
pInv.reset(new RealEstate(std::forward<Ts>(params)...));
|
||||
}
|
||||
return pInv; //同之前一样
|
||||
}
|
||||
```
|
||||
|
||||
我之前说过,当使用默认删除器时,你可以合理假设`std::unique_ptr`和原始指针大小相同。当自定义删除器时,情况可能不再如此。删除器是个函数指针,通常会使`std::unique_ptr`的字节从一个增加到两个。对于删除器的函数对象来说,大小取决于函数对象中存储的状态多少,无状态函数对象(比如没有捕获的lambda表达式)对大小没有影响,这意味当自定义删除器可以被lambda实现时,尽量使用lambda
|
||||
我之前说过,当使用默认删除器时(如`delete`),你可以合理假设`std::unique_ptr`对象和原始指针大小相同。当自定义删除器时,情况可能不再如此。函数指针形式的删除器,通常会使`std::unique_ptr`的从一个字(*word*)大小增加到两个。对于函数对象形式的删除器来说,变化的大小取决于函数对象中存储的状态多少,无状态函数(stateless function)对象(比如不捕获变量的*lambda*表达式)对大小没有影响,这意味当自定义删除器可以实现为函数或者*lambda*时,尽量使用*lambda*:
|
||||
|
||||
```cpp
|
||||
auto delInvmt = [](Investment* pInvestment)
|
||||
{
|
||||
makeLogEntry(pInvestment);
|
||||
delete pInvestment;
|
||||
};
|
||||
template<typename... Ts>
|
||||
std::unique_ptr<Investment, decltype(delInvmt)>
|
||||
makeInvestment(Ts&& params); //返回Investment*的大小
|
||||
auto delInvmt1 = [](Investment* pInvestment) //无状态lambda的
|
||||
{ //自定义删除器
|
||||
makeLogEntry(pInvestment);
|
||||
delete pInvestment;
|
||||
};
|
||||
|
||||
void delInvmt2(Investment* pInvestment)
|
||||
{
|
||||
makeLogEntry(pInvestment);
|
||||
delete pInvestment;
|
||||
template<typename... Ts> //返回类型大小是
|
||||
std::unique_ptr<Investment, decltype(delInvmt1)> //Investment*的大小
|
||||
makeInvestment(Ts&&... args);
|
||||
|
||||
void delInvmt2(Investment* pInvestment) //函数形式的
|
||||
{ //自定义删除器
|
||||
makeLogEntry(pInvestment);
|
||||
delete pInvestment;
|
||||
}
|
||||
template<typename... Ts>
|
||||
std::unique_ptr<Investment, void(*)(Investment*)>
|
||||
makeInvestment(Ts&&... params); //返回Investment*的指针加至少一个函数指针的大小
|
||||
template<typename... Ts> //返回类型大小是
|
||||
std::unique_ptr<Investment, void (*)(Investment*)> //Investment*的指针
|
||||
makeInvestment(Ts&&... params); //加至少一个函数指针的大小
|
||||
```
|
||||
|
||||
具有很多状态的自定义删除器会产生大尺寸`std::unique_ptr`对象。如果你发现自定义删除器使得你的`std::unique_ptr`变得过大,你需要审视修改你的设计。
|
||||
|
||||
工厂函数不是`std::unique_ptr`的唯一常见用法。作为实现**Pimpl Idiom**的一种机制,它更为流行。代码并不复杂,但是在某些情况下并不直观,所以这安排在Item22的专门主题中。
|
||||
工厂函数不是`std::unique_ptr`的唯一常见用法。作为实现**Pimpl Idiom**(译注:*pointer to implementation*,一种隐藏实际实现而减弱编译依赖性的设计思想,《Effective C++》条款31对此有过叙述)的一种机制,它更为流行。代码并不复杂,但是在某些情况下并不直观,所以这安排在[Item22](https://github.com/kelthuzadx/EffectiveModernCppChinese/blob/master/4.SmartPointers/item22.md)的专门主题中。
|
||||
|
||||
`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设计会自动匹配你的用法,比如`operator[]`就是数组对象,解引用操作符(`operator*`和`operator->`)就是单个对象专有。
|
||||
|
||||
数组的`std::unique_ptr`的存在应该不被使用,因为`std::array`, `std::vector`, `std::string`这些更好用的数据容器应该取代原始数组。原始数组的使用唯一情况是你使用类似C的API返回一个指向堆数组的原始指针。
|
||||
你应该对数组的`std::unique_ptr`的存在兴趣泛泛,因为`std::array`,`std::vector`,`std::string`这些更好用的数据容器应该取代原始数组。`std::unique_ptr<T[]>`有用的唯一情况是你使用类似C的API返回一个指向堆数组的原始指针,而你想接管这个数组的所有权。
|
||||
|
||||
`std::unique_ptr`是C++11中表示专有所有权的方法,但是其最吸引人的功能之一是它可以轻松高效的转换为`std::shared_ptr`:
|
||||
|
||||
```cpp
|
||||
std::shared_ptr<Investment> sp = makeInvestment(arguments);
|
||||
std::shared_ptr<Investment> sp = //将std::unique_ptr
|
||||
makeInvestment(arguments); //转为std::shared_ptr
|
||||
```
|
||||
|
||||
这就是为什么`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`的信息,请转到[Item19](https://github.com/kelthuzadx/EffectiveModernCppChinese/blob/master/4.SmartPointers/item19.md)。)
|
||||
|
||||
### 小结
|
||||
**请记住:**
|
||||
|
||||
- `std::unique_ptr`是轻量级、快速的、只能move的管理专有所有权语义资源的智能指针
|
||||
- 默认情况,资源销毁通过delete,但是支持自定义delete函数。有状态的删除器和函数指针会增加`std::unique_ptr`的大小
|
||||
- 将`std::unique_ptr`转化为`std::shared_ptr`是简单的
|
||||
- `std::unique_ptr`是轻量级、快速的、只可移动(*move-only*)的管理专有所有权语义资源的智能指针
|
||||
- 默认情况,资源销毁通过`delete`实现,但是支持自定义删除器。有状态的删除器和函数指针会增加`std::unique_ptr`对象的大小
|
||||
- 将`std::unique_ptr`转化为`std::shared_ptr`非常简单
|
||||
|
@ -1,156 +1,160 @@
|
||||
## Item 19:对于共享资源使用std::shared_ptr
|
||||
条款十九:对于共享资源使用std::shared_ptr
|
||||
## 条款十九:对于共享资源使用`std::shared_ptr`
|
||||
**Item 19: Use `std::shared_ptr` for shared-ownership resource management**
|
||||
|
||||
程序员使用带垃圾回收的语言指着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`会销毁它所指向的对象。就垃圾回收来说,客户端不需要关心指向对象的生命周期,而对象的析构是确定性的。
|
||||
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`指向该资源。`std::shared_ptr`构造函数递增引用计数值(注意是通常——原因参见下面),析构函数递减值,拷贝赋值运算符可能递增也可能递减值。(如果sp1和sp2是`std::shared_ptr`并且指向不同对象,赋值运算符`sp1=sp2`会使sp1指向sp2指向的对象。直接效果就是sp1引用计数减一,sp2引用计数加一。)如果`std::shared_ptr`发现引用计数值为零,没有其他`std::shared_ptr`指向该资源,它就会销毁资源。
|
||||
`std::shared_ptr`通过引用计数(*reference count*)来确保它是否是最后一个指向某种资源的指针,引用计数关联资源并跟踪有多少`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`大小是原始指针的两倍**,因为它内部包含一个指向资源的原始指针,还包含一个资源的引用计数值。
|
||||
+ **引用计数必须动态分配**。 理论上,引用计数与所指对象关联起来,但是被指向的对象不知道这件事情(译注:不知道有指向自己的指针)。因此它们没有办法存放一个引用计数值。Item21会解释使用`std::make_shared`创建`std::shared_ptr`可以避免引用计数的动态分配,但是还存在一些`std::make_shared`不能使用的场景,这时候引用计数就会动态分配。
|
||||
+ **递增递减引用计数必须是原子性的**,因为多个reader、writer可能在不同的线程。比如,指向某种资源的`std::shared_ptr`可能在一个线程执行析构,在另一个不同的线程,`std::shared_ptr`指向相同的对象,但是执行的确是拷贝操作。原子操作通常比非原子操作要慢,所以即使是引用计数,你也应该假定读写它们是存在开销的。
|
||||
+ **`std::shared_ptr`大小是原始指针的两倍**,因为它内部包含一个指向资源的原始指针,还包含一个指向资源的引用计数值的原始指针。(这种实现法并不是标准要求的,但是我(指原书作者Scott Meyers)熟悉的所有标准库都这样实现。)
|
||||
+ **引用计数的内存必须动态分配**。 概念上,引用计数与所指对象关联起来,但是实际上被指向的对象不知道这件事情(译注:不知道有一个关联到自己的计数值)。因此它们没有办法存放一个引用计数值。(一个好消息是任何对象——甚至是内置类型的——都可以由`std::shared_ptr`管理。)[Item21](https://github.com/kelthuzadx/EffectiveModernCppChinese/blob/master/4.SmartPointers/item21.md)会解释使用`std::make_shared`创建`std::shared_ptr`可以避免引用计数的动态分配,但是还存在一些`std::make_shared`不能使用的场景,这时候引用计数就会动态分配。
|
||||
+ **递增递减引用计数必须是原子性的**,因为多个reader、writer可能在不同的线程。比如,指向某种资源的`std::shared_ptr`可能在一个线程执行析构(于是递减指向的对象的引用计数),在另一个不同的线程,`std::shared_ptr`指向相同的对象,但是执行的却是拷贝操作(因此递增了同一个引用计数)。原子操作通常比非原子操作要慢,所以即使引用计数通常只有一个*word*大小,你也应该假定读写它们是存在开销的。
|
||||
|
||||
我写道`std::shared_ptr`构造函数只是“通常”递增指向对象的引用计数会不会让你有点好奇?创建一个指向对象的`std::shared_ptr`至少产生了一个指向对象的智能指针,为什么我没说**总是**增加引用计数值?
|
||||
我写道`std::shared_ptr`构造函数只是“通常”递增指向对象的引用计数会不会让你有点好奇?创建一个指向对象的`std::shared_ptr`就产生了又一个指向那个对象的`std::shared_ptr`,为什么我没说**总是**增加引用计数值?
|
||||
|
||||
原因是移动构造函数的存在。从另一个`std::shared_ptr`移动构造新`std::shared_ptr`会将原来的`std::shared_ptr`设置为null,那意味着老的`std::shared_ptr`不再指向资源,同时新的`std::shared_ptr`指向资源。这样的结果就是不需要修改引用计数值。因此移动`std::shared_ptr`会比拷贝它要快:拷贝要求递增引用计数值,移动不需要。移动赋值运算符同理,所以移动赋值运算符也比拷贝赋值运算符快。
|
||||
原因是移动构造函数的存在。从另一个`std::shared_ptr`移动构造新`std::shared_ptr`会将原来的`std::shared_ptr`设置为null,那意味着老的`std::shared_ptr`不再指向资源,同时新的`std::shared_ptr`指向资源。这样的结果就是不需要修改引用计数值。因此移动`std::shared_ptr`会比拷贝它要快:拷贝要求递增引用计数值,移动不需要。移动赋值运算符同理,所以移动构造比拷贝构造快,移动赋值运算符也比拷贝赋值运算符快。
|
||||
|
||||
类似`std::unique_ptr`(参加Item18),`std::shared_ptr`使用**delete**作为资源的默认销毁器,但是它也支持自定义的销毁器。这种支持有别于`std::unique_ptr`。对于`std::unique_ptr`来说,销毁器类型是智能指针类型的一部分。对于`std::shared_ptr`则不是:
|
||||
类似`std::unique_ptr`(参见[Item18](https://github.com/kelthuzadx/EffectiveModernCppChinese/blob/master/4.SmartPointers/item18.md)),`std::shared_ptr`使用`delete`作为资源的默认销毁机制,但是它也支持自定义的删除器。这种支持有别于`std::unique_ptr`。对于`std::unique_ptr`来说,删除器类型是智能指针类型的一部分。对于`std::shared_ptr`则不是:
|
||||
```CPP
|
||||
auto loggingDel = [](Widget *pw) //自定义销毁器
|
||||
{ // (和Item 18一样)
|
||||
makeLogEntry(pw);
|
||||
delete pw;
|
||||
};
|
||||
auto loggingDel = [](Widget *pw) //自定义删除器
|
||||
{ //(和条款18一样)
|
||||
makeLogEntry(pw);
|
||||
delete pw;
|
||||
};
|
||||
|
||||
std::unique_ptr< // 销毁器类型是
|
||||
Widget, decltype(loggingDel) // ptr类型的一部分
|
||||
> upw(new Widget, loggingDel);
|
||||
std::shared_ptr<Widget> // 销毁器类型不是
|
||||
spw(new Widget, loggingDel); // ptr类型的一部分
|
||||
std::unique_ptr< //删除器类型是
|
||||
Widget, decltype(loggingDel) //指针类型的一部分
|
||||
> upw(new Widget, loggingDel);
|
||||
std::shared_ptr<Widget> //删除器类型不是
|
||||
spw(new Widget, loggingDel); //指针类型的一部分
|
||||
```
|
||||
`std::shared_ptr`的设计更为灵活。考虑有两个`std::shared_ptr`,每个自带不同的销毁器(比如通过lambda表达式自定义销毁器):
|
||||
`std::shared_ptr`的设计更为灵活。考虑有两个`std::shared_ptr<Widget>`,每个自带不同的删除器(比如通过*lambda*表达式自定义删除器):
|
||||
|
||||
```CPP
|
||||
auto customDeleter1 = [](Widget *pw) { … };
|
||||
auto customDeleter2 = [](Widget *pw) { … };
|
||||
auto customDeleter1 = [](Widget *pw) { … }; //自定义删除器,
|
||||
auto customDeleter2 = [](Widget *pw) { … }; //每种类型不同
|
||||
std::shared_ptr<Widget> pw1(new Widget, customDeleter1);
|
||||
std::shared_ptr<Widget> pw2(new Widget, customDeleter2);
|
||||
```
|
||||
因为**pw1**和**pw2**有相同的类型,所以它们都可以放到存放那个类型的对象的容器中:
|
||||
因为`pw1`和`pw2`有相同的类型,所以它们都可以放到存放那个类型的对象的容器中:
|
||||
```CPP
|
||||
std::vector<std::shared_ptr<Widget>> vpw{ pw1, pw2 };
|
||||
```
|
||||
它们也能相互赋值,也可以传入形参为`std::shared_ptr<Widget>`的函数。但是`std::unique_ptr`就不行,因为`std::unique_ptr`把销毁器视作类型的一部分。
|
||||
它们也能相互赋值,也可以传入一个形参为`std::shared_ptr<Widget>`的函数。但是自定义删除器类型不同的`std::unique_ptr`就不行,因为`std::unique_ptr`把删除器视作类型的一部分。
|
||||
|
||||
另一个不同于`std::unique_ptr`的地方是,指定自定义销毁器不会改变`std::shared_ptr`对象的大小。不管销毁器是什么,一个`std::shared_ptr`对象都是两个指针大小。这是个好消息,但是它应该让你隐隐约约不安。自定义销毁器可以是函数对象,函数对象可以包含任意多的数据。它意味着函数对象是任意大的。`std::shared_ptr`怎么能引用一个任意大的销毁器而不使用更多的内存?
|
||||
另一个不同于`std::unique_ptr`的地方是,指定自定义删除器不会改变`std::shared_ptr`对象的大小。不管删除器是什么,一个`std::shared_ptr`对象都是两个指针大小。这是个好消息,但是它应该让你隐隐约约不安。自定义删除器可以是函数对象,函数对象可以包含任意多的数据。它意味着函数对象是任意大的。`std::shared_ptr`怎么能引用一个任意大的删除器而不使用更多的内存?
|
||||
|
||||
它不能。它必须使用更多的内存。然而,那部分内存不是`std::shared_ptr`对象的一部分。那部分在堆上面,只要`std::shared_ptr`自定义了分配器,那部分内存随便在哪都行。我前面提到了`std::shared_ptr`对象包含了所指对象的引用计数。没错,但是有点误导人。因为引用计数是另一个更大的数据结构的一部分,那个数据结构通常叫做**控制块**(control block)。控制块包含除了引用计数值外的一个自定义销毁器的拷贝,当然前提是存在自定义销毁器。如果用户还指定了自定义分配器,控制器也会包含一个分配器的拷贝。控制块可能还包含一些额外的数据,正如Item21提到的,一个次级引用计数weak count,但是目前我们先忽略它。我们可以想象`std::shared_ptr`对象在内存中是这样:
|
||||
它不能。它必须使用更多的内存。然而,那部分内存不是`std::shared_ptr`对象的一部分。那部分在堆上面,或者`std::shared_ptr`创建者利用`std::shared_ptr`对自定义分配器的支持能力,那部分内存随便在哪都行。我前面提到了`std::shared_ptr`对象包含了所指对象的引用计数的指针。没错,但是有点误导人。因为引用计数是另一个更大的数据结构的一部分,那个数据结构通常叫做**控制块**(*control block*)。每个`std::shared_ptr`管理的对象都有个相应的控制块。控制块除了包含引用计数值外还有一个自定义删除器的拷贝,当然前提是存在自定义删除器。如果用户还指定了自定义分配器,控制块也会包含一个分配器的拷贝。控制块可能还包含一些额外的数据,正如[Item21](https://github.com/kelthuzadx/EffectiveModernCppChinese/blob/master/4.SmartPointers/item21.md)提到的,一个次级引用计数*weak count*,但是目前我们先忽略它。我们可以想象`std::shared_ptr`对象在内存中是这样:
|
||||
|
||||

|
||||

|
||||
|
||||
当`std::shared_ptr`对象一创建,对象控制块就建立了。至少我们期望是如此。通常,对于一个创建指向对象的`std::shared_ptr`的函数来说不可能知道是否有其他`std::shared_ptr`早已指向那个对象,所以控制块的创建会遵循下面几条规则:
|
||||
当指向对象的`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::shared_ptr`时会创建控制块**。如果你想从一个早已存在控制块的对象上创建`std::shared_ptr`,你将假定传递一个`std::shared_ptr`或者`std::weak_ptr`作为构造函数实参,而不是原始指针。用`std::shared_ptr`或者`std::weak_ptr`作为构造函数实参创建`std::shared_ptr`不会创建新控制块,因为它可以依赖传递来的智能指针指向控制块。
|
||||
+ **`std::make_shared`(参见[Item21](https://github.com/kelthuzadx/EffectiveModernCppChinese/blob/master/4.SmartPointers/item21.md))总是创建一个控制块**。它创建一个要指向的新对象,所以可以肯定`std::make_shared`调用时对象不存在其他控制块。
|
||||
+ **当从独占指针(即`std::unique_ptr`或者`std::auto_ptr`)上构造出`std::shared_ptr`时会创建控制块**。独占指针没有使用控制块,所以指针指向的对象没有关联控制块。(作为构造的一部分,`std::shared_ptr`侵占独占指针所指向的对象的独占权,所以独占指针被设置为null)
|
||||
+ **当从原始指针上构造出`std::shared_ptr`时会创建控制块**。如果你想从一个早已存在控制块的对象上创建`std::shared_ptr`,你将假定传递一个`std::shared_ptr`或者`std::weak_ptr`(参见[Item20](https://github.com/kelthuzadx/EffectiveModernCppChinese/blob/master/4.SmartPointers/item20.md))作为构造函数实参,而不是原始指针。用`std::shared_ptr`或者`std::weak_ptr`作为构造函数实参创建`std::shared_ptr`不会创建新控制块,因为它可以依赖传递来的智能指针指向控制块。
|
||||
|
||||
这些规则造成的后果就是从原始指针上构造超过一个`std::shared_ptr`就会让你走上未定义行为的快车道,因为指向的对象有多个控制块关联。多个控制块意味着多个引用计数值,多个引用计数值意味着对象将会被销毁多次(每个引用计数一次)。那意味着下面的代码是有问题的,很有问题,问题很大:
|
||||
这些规则造成的后果就是从原始指针上构造超过一个`std::shared_ptr`就会让你走上未定义行为的快车道,因为指向的对象有多个控制块关联。多个控制块意味着多个引用计数值,多个引用计数值意味着对象将会被销毁多次(每个引用计数一次)。那意味着像下面的代码是有问题的,很有问题,问题很大:
|
||||
```cpp
|
||||
auto pw = new Widget; // pw是原始指针
|
||||
auto pw = new Widget; //pw是原始指针
|
||||
…
|
||||
std::shared_ptr<Widget> spw1(pw, loggingDel); // 为*pw创建控制块
|
||||
std::shared_ptr<Widget> spw1(pw, loggingDel); //为*pw创建控制块
|
||||
…
|
||||
std::shared_ptr<Widget> spw2(pw, loggingDel); // 为*pw创建第二个控制块
|
||||
std::shared_ptr<Widget> spw2(pw, loggingDel); //为*pw创建第二个控制块
|
||||
```
|
||||
创建原始指针指向动态分配的对象很糟糕,因为它完全背离了这章的建议:对于共享资源使用`std::shared_ptr`而不是原始指针。(如果你忘记了该建议的动机,请翻到115页)。撇开那个不说,创建**pw**那一行代码虽然让人厌恶,但是至少不会造成未定义程序行为。
|
||||
创建原始指针`pw`指向动态分配的对象很糟糕,因为它完全背离了这章的建议:倾向于使用智能指针而不是原始指针。(如果你忘记了该建议的动机,请翻到[本章开头](https://github.com/kelthuzadx/EffectiveModernCppChinese/blob/master/4.SmartPointers/item18.md))。撇开那个不说,创建`pw`那一行代码虽然让人厌恶,但是至少不会造成未定义程序行为。
|
||||
|
||||
现在,传给**spw1**的构造函数一个原始指针,它会为指向的对象创建一个控制块(引用计数值在里面)。这种情况下,指向的对象是`*pw`。就其本身而言没什么问题,但是将同样的原始指针传递给**spw2**的构造函数会再次为`*pw`创建一个控制块。因此`*pw`有两个引用计数值,每一个最后都会变成零,然后最终导致`*pw`销毁两次。第二个销毁会产生未定义行为。
|
||||
现在,传给`spw1`的构造函数一个原始指针,它会为指向的对象创建一个控制块(因此有个引用计数值)。这种情况下,指向的对象是`*pw`(即`pw`指向的对象)。就其本身而言没什么问题,但是将同样的原始指针传递给`spw2`的构造函数会再次为`*pw`创建一个控制块(所以也有个引用计数值)。因此`*pw`有两个引用计数值,每一个最后都会变成零,然后最终导致`*pw`销毁两次。第二个销毁会产生未定义行为。
|
||||
|
||||
`std::shared_ptr`给我们上了两堂课。第一,避免传给`std::shared_ptr`构造函数原始指针。通常替代方案是使用`std::make_shared`(参见[Item21](https://github.com/kelthuzadx/EffectiveModernCppChinese/blob/master/4.SmartPointers/item21.md)),不过上面例子中,我们使用了自定义删除器,用`std::make_shared`就没办法做到。第二,如果你必须传给`std::shared_ptr`构造函数原始指针,直接传`new`出来的结果,不要传指针变量。如果上面代码第一部分这样重写:
|
||||
|
||||
`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);
|
||||
std::shared_ptr<Widget> spw1(new Widget, //直接使用new的结果
|
||||
loggingDel);
|
||||
```
|
||||
会少了很多创建第二个从原始指针上构造`std::shared_ptr`的诱惑。相应的,创建spw2也会很自然的用spw1作为初始化参数(即用`std::shared_ptr`拷贝构造),那就没什么问题了:
|
||||
会少了很多从原始指针上构造第二个`std::shared_ptr`的诱惑。相应的,创建`spw2`也会很自然的用`spw1`作为初始化参数(即用`std::shared_ptr`拷贝构造函数),那就没什么问题了:
|
||||
```CPP
|
||||
std::shared_ptr<Widget> spw2(spw1); // spw2使用spw1一样的控制块
|
||||
std::shared_ptr<Widget> spw2(spw1); //spw2使用spw1一样的控制块
|
||||
```
|
||||
一个尤其令人意外的地方是使用**this**原始指针作为`std::shared_ptr`构造函数实参的时候可能导致创建多个控制块。假设我们的程序使用`std::shared_ptr`管理**Widget**对象,我们有一个数据结构用于跟踪已经处理过的**Widget**对象:
|
||||
一个尤其令人意外的地方是使用`this`指针作为`std::shared_ptr`构造函数实参的时候可能导致创建多个控制块。假设我们的程序使用`std::shared_ptr`管理`Widget`对象,我们有一个数据结构用于跟踪已经处理过的`Widget`对象:
|
||||
```cpp
|
||||
std::vector<std::shared_ptr<Widget>> processedWidgets;
|
||||
```
|
||||
继续,假设**Widget**有一个用于处理的成员函数:
|
||||
继续,假设`Widget`有一个用于处理的成员函数:
|
||||
```cpp
|
||||
class Widget {
|
||||
public:
|
||||
…
|
||||
void process();
|
||||
…
|
||||
…
|
||||
void process();
|
||||
…
|
||||
};
|
||||
```
|
||||
对于**Widget::process**看起来合理的代码如下:
|
||||
对于`Widget::process`看起来合理的代码如下:
|
||||
```cpp
|
||||
void Widget::process()
|
||||
{
|
||||
… // 处理Widget
|
||||
processedWidgets.emplace_back(this); // 然后将他加到已处理过的Widget的列表中
|
||||
// 这是错的
|
||||
}
|
||||
… //处理Widget
|
||||
processedWidgets.emplace_back(this); //然后将它加到已处理过的Widget
|
||||
} //的列表中,这是错的!
|
||||
```
|
||||
评论已经说了这是错的——或者至少大部分是错的。(错误的部分是传递this,而不是使用了**emplace_back**。如果你不熟悉**emplace_back**,参见Item42)。上面的代码可以通过编译,但是向容器传递一个原始指针(this),`std::shared_ptr`会由此为指向的对象(`*this`)创建一个控制块。那看起来没什么问题,直到你意识到如果成员函数外面早已存在指向**Widget**对象的指针,它是未定义行为的Game, Set, and Match(译注:一部电影,但是译者没看过。。。)。
|
||||
注释已经说了这是错的——或者至少大部分是错的。(错误的部分是传递`this`,而不是使用了`emplace_back`。如果你不熟悉`emplace_back`,参见[Item42](https://github.com/kelthuzadx/EffectiveModernCppChinese/blob/master/8.Tweaks/item42.md))。上面的代码可以通过编译,但是向`std::shared_ptr`的容器传递一个原始指针(`this`),`std::shared_ptr`会由此为指向的`Widget`(`*this`)创建一个控制块。那看起来没什么问题,直到你意识到如果成员函数外面早已存在指向那个`Widget`对象的指针,它是未定义行为的Game, Set, and Match(译注:一部关于网球的电影,但是译者没看过。句子本意“压倒性胜利;比赛结束”)。
|
||||
|
||||
`std::shared_ptr`API已有处理这种情况的设施。它的名字可能是C++标准库中最奇怪的一个:`std::enable_shared_from_this`。如果你想创建一个用`std::shared_ptr`管理的类,这个类能够用`this`指针安全地创建一个`std::shared_ptr`,`std::enable_shared_from_this`就可作为基类的模板类。在我们的例子中,`Widget`将会继承自`std::enable_shared_from_this`:
|
||||
|
||||
`std::shared_ptr`API已有处理这种情况的设施。它的名字可能是C++标准库中最奇怪的一个:`std::enable_shared_from_this`。它是一个用做基类的模板类,模板类型参数是某个想被`std::shared_ptr`管理且能从该类型的**this**对象上安全创建`std::shared_ptr`指针的存在。在我们的例子中,**Widget**将会继承自`std::enable_shared_from_this`:
|
||||
```cpp
|
||||
class Widget: public std::enable_shared_from_this<Widget> {
|
||||
public:
|
||||
…
|
||||
void process();
|
||||
…
|
||||
…
|
||||
void process();
|
||||
…
|
||||
};
|
||||
```
|
||||
正如我所说,`std::enable_shared_from_this`是一个用作基类的模板类。它的模板参数总是某个继承自它的类,所以**Widget**继承自`std::enable_shared_from_this<Widget>`。如果某类型继承自一个由该类型(译注:作为模板类型参数)进行模板化得到的基类这个东西让你心脏有点遭不住,别去想它就好了。代码完全合法,而且它背后的设计模式也是没问题的,并且这种设计模式还有个标准名字,尽管该名字和`std::enable_shared_from_this`一样怪异。这个标准名字就是奇异递归模板模式(The Curiously Recurring Template Pattern(CRTP))。如果你想学更多关于它的内容,请搜索引擎一展身手,现在我们要回到`std::enable_shared_from_this`上。
|
||||
正如我所说,`std::enable_shared_from_this`是一个基类模板。它的模板参数总是某个继承自它的类,所以`Widget`继承自`std::enable_shared_from_this<Widget>`。如果某类型继承自一个由该类型(译注:作为模板类型参数)进行模板化得到的基类这个东西让你心脏有点遭不住,别去想它就好了。代码完全合法,而且它背后的设计模式也是没问题的,并且这种设计模式还有个标准名字,尽管该名字和`std::enable_shared_from_this`一样怪异。这个标准名字就是奇异递归模板模式(*The Curiously Recurring Template Pattern*(*CRTP*))。如果你想学更多关于它的内容,请搜索引擎一展身手,现在我们要回到`std::enable_shared_from_this`上。
|
||||
|
||||
`std::enable_shared_from_this`定义了一个成员函数,成员函数会创建指向当前对象的`std::shared_ptr`却不创建多余控制块。这个成员函数就是`shared_from_this`,无论在哪当你想在成员函数中使用`std::shared_ptr`指向`this`所指对象时都请使用它。这里有个`Widget::process`的安全实现:
|
||||
|
||||
`std::enable_shared_from_this`定义了一个成员函数,成员函数会创建指向当前对象的`std::shared_ptr`却不创建多余控制块。这个成员函数就是`shared_from_this`,无论在哪当你想使用`std::shared_ptr`指向this所指对象时都请使用它。这里有个`Widget::process`的安全实现:
|
||||
```cpp
|
||||
void Widget::process()
|
||||
{
|
||||
// 和之前一样,处理Widget
|
||||
…
|
||||
// 把指向当前对象的shared_ptr加入processedWidgets
|
||||
processedWidgets.emplace_back(shared_from_this());
|
||||
//和之前一样,处理Widget
|
||||
…
|
||||
//把指向当前对象的std::shared_ptr加入processedWidgets
|
||||
processedWidgets.emplace_back(shared_from_this());
|
||||
}
|
||||
```
|
||||
从内部来说,`shared_from_this`查找当前对象控制块,然后创建一个新的`std::shared_ptr`指向这个控制块。设计的依据是当前对象已经存在一个关联的控制块。要想符合设计依据的情况,必须已经存在一个指向当前对象的`std::shared_ptr`(即调用shared_from_this的成员函数外面已经存在一个`std::shared_ptr`)。如果没有`std::shared_ptr`指向当前对象(即当前对象没有关联控制块),行为是未定义的,shared_from_this通常抛出一个异常。
|
||||
从内部来说,`shared_from_this`查找当前对象控制块,然后创建一个新的`std::shared_ptr`关联这个控制块。设计的依据是当前对象已经存在一个关联的控制块。要想符合设计依据的情况,必须已经存在一个指向当前对象的`std::shared_ptr`(比如调用`shared_from_this`的成员函数外面已经存在一个`std::shared_ptr`)。如果没有`std::shared_ptr`指向当前对象(即当前对象没有关联控制块),行为是未定义的,`shared_from_this`通常抛出一个异常。
|
||||
|
||||
要想防止客户端在调用`std::shared_ptr`前先调用`shared_from_this`,继承自`std::enable_shared_from_this`的类通常将它们的构造函数声明为private,并且让客户端通过工厂方法创建`std::shared_ptr`。以**Widget**为例,代码可以是这样:
|
||||
要想防止客户端在存在一个指向对象的`std::shared_ptr`前先调用含有`shared_from_this`的成员函数,继承自`std::enable_shared_from_this`的类通常将它们的构造函数声明为`private`,并且让客户端通过返回`std::shared_ptr`的工厂函数创建对象。以`Widget`为例,代码可以是这样:
|
||||
```cpp
|
||||
class Widget: public std::enable_shared_from_this<Widget> {
|
||||
public:
|
||||
// 完美转发参数的工厂方法
|
||||
template<typename... Ts>
|
||||
static std::shared_ptr<Widget> create(Ts&&... params);
|
||||
…
|
||||
void process(); // 和前面一样
|
||||
…
|
||||
//完美转发参数给private构造函数的工厂函数
|
||||
template<typename... Ts>
|
||||
static std::shared_ptr<Widget> create(Ts&&... params);
|
||||
…
|
||||
void process(); //和前面一样
|
||||
…
|
||||
private:
|
||||
…
|
||||
… //构造函数
|
||||
};
|
||||
```
|
||||
现在,你可能隐约记得我们讨论控制块的动机是想了解`std::shared_ptr`关联一个控制块的成本。既然我们已经知道了怎么避免创建过多控制块,就让我们回到原来的主题。
|
||||
现在,你可能隐约记得我们讨论控制块的动机是想了解有关`std::shared_ptr`的成本。既然我们已经知道了怎么避免创建过多控制块,就让我们回到原来的主题。
|
||||
|
||||
控制块通常只占几个word大小,自定义销毁器和分配器可能会让它变大一点。通常控制块的实现比你想的更复杂一些。它使用继承,甚至里面还有一个虚函数(用来确保指向的对象被正确销毁)。这意味着使用`std::shared_ptr`还会招致控制块使用虚函数带来的成本。
|
||||
控制块通常只占几个*word*大小,自定义删除器和分配器可能会让它变大一点。通常控制块的实现比你想的更复杂一些。它使用继承,甚至里面还有一个虚函数(用来确保指向的对象被正确销毁)。这意味着使用`std::shared_ptr`还会招致控制块使用虚函数带来的成本。
|
||||
|
||||
了解了动态分配控制块,任意大小的销毁器和分配器,虚函数机制,原子引用计数修改,你对于`std::shared_ptr`的热情可能有点消退。可以理解,对每个资源管理问题来说都没有最佳的解决方案。但就它提供的功能来说,`std::shared_ptr`的开销是非常合理的。在通常情况下,`std::shared_ptr`创建控制块会使用默认销毁器和默认分配器,控制块只需三个word大小。它的分配基本上是无开销的。(开销被并入了指向的对象的分配成本里。细节参见Item21)。对`std::shared_ptr`解引用的开销不会比原始指针高。执行原子引用计数修改操作需要承担一两个原子操作开销,这些操作通常都会一一映射到机器指令上,所以即使对比非原子指令来说,原子指令开销较大,但是它们仍然只是单个指令。对于每个被`std::shared_ptr`指向的对象来说,控制块中的虚函数机制产生的开销通常只需要承受一次,即对象销毁的时候。
|
||||
了解了动态分配控制块,任意大小的删除器和分配器,虚函数机制,原子性的引用计数修改,你对于`std::shared_ptr`的热情可能有点消退。可以理解,对每个资源管理问题来说都没有最佳的解决方案。但就它提供的功能来说,`std::shared_ptr`的开销是非常合理的。在通常情况下,使用默认删除器和默认分配器,使用`std::make_shared`创建`std::shared_ptr`,产生的控制块只需三个word大小。它的分配基本上是无开销的。(开销被并入了指向的对象的分配成本里。细节参见[Item21](https://github.com/kelthuzadx/EffectiveModernCppChinese/blob/master/4.SmartPointers/item21.md))。对`std::shared_ptr`解引用的开销不会比原始指针高。执行需要原子引用计数修改的操作需要承担一两个原子操作开销,这些操作通常都会一一映射到机器指令上,所以即使对比非原子指令来说,原子指令开销较大,但是它们仍然只是单个指令上的。对于每个被`std::shared_ptr`指向的对象来说,控制块中的虚函数机制产生的开销通常只需要承受一次,即对象销毁的时候。
|
||||
|
||||
作为这些轻微开销的交换,你得到了动态分配的资源的生命周期自动管理的好处。大多数时候,比起手动管理,使用`std::shared_ptr`管理共享性资源都是非常合适的。如果你还在犹豫是否能承受`std::shared_ptr`带来的开销,那就再想想你是否需要共享资源。如果独占资源可行或者可能可行,用`std::unique_ptr`是一个更好的选择。它的性能profile更接近于原始指针,并且从`std::unique_ptr`升级到`std::shared_ptr`也很容易,因为`std::shared_ptr`可以从`std::unique_ptr`上创建。
|
||||
作为这些轻微开销的交换,你得到了动态分配的资源的生命周期自动管理的好处。大多数时候,比起手动管理,使用`std::shared_ptr`管理共享性资源都是非常合适的。如果你还在犹豫是否能承受`std::shared_ptr`带来的开销,那就再想想你是否需要共享所有权。如果独占资源可行或者**可能**可行,用`std::unique_ptr`是一个更好的选择。它的性能表现更接近于原始指针,并且从`std::unique_ptr`升级到`std::shared_ptr`也很容易,因为`std::shared_ptr`可以从`std::unique_ptr`上创建。
|
||||
|
||||
反之不行。当你的资源由`std::shared_ptr`管理,现在又想修改资源生命周期管理方式是没有办法的。即使引用计数为一,你也不能重新修改资源所有权,改用`std::unique_ptr`管理它。所有权和`std::shared_ptr`指向的资源之前签订的协议是“除非死亡否则永不分离”。不能离婚,不能废除,没有特许。
|
||||
反之不行。当你的资源由`std::shared_ptr`管理,现在又想修改资源生命周期管理方式是没有办法的。即使引用计数为一,你也不能重新修改资源所有权,改用`std::unique_ptr`管理它。资源和指向它的`std::shared_ptr`的签订的所有权协议是“除非死亡否则永不分开”。不能分离,不能废除,没有特许。
|
||||
|
||||
`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`的API设计之初就是针对单个对象的,没有办法`std::shared_ptr<T[]>`。一次又一次,“聪明”的程序员踌躇于是否该使用`std::shared_ptr<T>`指向数组,然后传入自定义删除器来删除数组(即`delete []`)。这可以通过编译,但是是一个糟糕的主意。一方面,`std::shared_ptr`没有提供`operator[]`,所以数组索引操作需要借助怪异的指针算术。另一方面,`std::shared_ptr`支持转换为指向基类的指针,这对于单个对象来说有效,但是当用于数组类型时相当于在类型系统上开洞。(出于这个原因,`std::unique_ptr<T[]>` API禁止这种转换。)更重要的是,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::unique_ptr`,`std::shared_ptr`对象通常大两倍,控制块会产生开销,需要原子性的引用计数修改操作。
|
||||
+ 默认资源销毁是通过`delete`,但是也支持自定义删除器。删除器的类型是什么对于`std::shared_ptr`的类型没有影响。
|
||||
+ 避免从原始指针变量上创建`std::shared_ptr`。
|
||||
|
@ -1,90 +1,91 @@
|
||||
## Item 20: 当std::shard_ptr可能悬空时使用std::weak_ptr
|
||||
## 条款二十:当`std::shard_ptr`可能悬空时使用`std::weak_ptr`
|
||||
|
||||
**Item 20: Use `std::weak_ptr` for `std::shared_ptr`-like pointers that can dangle**
|
||||
|
||||
自相矛盾的是,如果有一个像`std::shared_ptr`的指针但是不参与资源所有权共享的指针是很方便的。换句话说,是一个类似`std::shared_ptr`但不影响对象引用计数的指针。这种类型的智能指针必须要解决一个`std::shared_ptr`不存在的问题:可能指向已经销毁的对象。一个真正的智能指针应该跟踪所值对象,在悬空时知晓,悬空(*dangle*)就是指针指向的对象不再存在。这就是对`std::weak_ptr`最精确的描述。
|
||||
自相矛盾的是,如果有一个像`std::shared_ptr`(见[Item19](https://github.com/kelthuzadx/EffectiveModernCppChinese/blob/master/4.SmartPointers/item19.md))的但是不参与资源所有权共享的指针是很方便的。换句话说,是一个类似`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`的增强。
|
||||
你可能想知道什么时候该用`std::weak_ptr`。你可能想知道关于`std::weak_ptr` API的更多。它什么都好除了不太智能。`std::weak_ptr`不能解引用,也不能测试是否为空值。因为`std::weak_ptr`不是一个独立的智能指针。它是`std::shared_ptr`的增强。
|
||||
|
||||
这种关系在它创建之时就建立了。`std::weak_ptr`通常从`std::shared_ptr`上创建。当从`std::shared_ptr`上创建`std::weak_ptr`时两者指向相同的对象,但是`std::weak_ptr`不会影响所指对象的引用计数:
|
||||
```cpp
|
||||
auto spw = // after spw is constructed
|
||||
std::make_shared<Widget>(); // the pointed-to Widget's
|
||||
// ref count(RC) is 1
|
||||
// See Item 21 for in on std::make_shared
|
||||
auto spw = //spw创建之后,指向的Widget的
|
||||
std::make_shared<Widget>(); //引用计数(ref count,RC)为1。
|
||||
//std::make_shared的信息参见条款21
|
||||
…
|
||||
std::weak_ptr<Widget> wpw(spw); // wpw points to same Widget as spw. RC remains 1
|
||||
std::weak_ptr<Widget> wpw(spw); //wpw指向与spw所指相同的Widget。RC仍为1
|
||||
…
|
||||
spw = nullptr; // RC goes to 0, and the
|
||||
// Widget is destroyed.
|
||||
// wpw now dangles
|
||||
spw = nullptr; //RC变为0,Widget被销毁。
|
||||
//wpw现在悬空
|
||||
```
|
||||
`std::weak_ptr`用**expired**来表示已经dangle。你可以用它直接做测试:
|
||||
悬空的`std::weak_ptr`被称作已经**expired**(过期)。你可以用它直接做测试:
|
||||
|
||||
```CPP
|
||||
if (wpw.expired()) … // if wpw doesn't point to an object
|
||||
if (wpw.expired()) … //如果wpw没有指向对象…
|
||||
```
|
||||
但是通常你期望的是检查`std::weak_ptr`是否已经失效,如果没有失效则访问其指向的对象。这做起来比较容易。因为缺少解引用操作,没有办法写这样的代码。即使有,将检查和解引用分开会引入竞态条件:在调用**expired**和解引用操作之间,另一个线程可能对指向的对象重新赋值或者析构,并由此造成对象已析构。这种情况下,你的解引用将会产生未定义行为。
|
||||
但是通常你期望的是检查`std::weak_ptr`是否已经过期,如果没有过期则访问其指向的对象。这做起来可不是想着那么简单。因为缺少解引用操作,没有办法写这样的代码。即使有,将检查和解引用分开会引入竞态条件:在调用`expired`和解引用操作之间,另一个线程可能对指向这对象的`std::shared_ptr`重新赋值或者析构,并由此造成对象已析构。这种情况下,你的解引用将会产生未定义行为。
|
||||
|
||||
你需要的是一个原子操作实现检查是否过期,如果没有过期就访问所指对象。这可以通过从`std::weak_ptr`创建`std::shared_ptr`来实现,具体有两种形式可以从`std::weak_ptr`上创建`std::shared_ptr`,具体用哪种取决于`std::weak_ptr`过期时你希望`std::shared_ptr`表现出什么行为。一种形式是`std::weak_ptr::lock`,它返回一个`std::shared_ptr`,如果`std::weak_ptr`过期这个`std::shared_ptr`为空:
|
||||
你需要的是一个原子操作检查`std::weak_ptr`是否已经过期,如果没有过期就访问所指对象。这可以通过从`std::weak_ptr`创建`std::shared_ptr`来实现,具体有两种形式可以从`std::weak_ptr`上创建`std::shared_ptr`,具体用哪种取决于`std::weak_ptr`过期时你希望`std::shared_ptr`表现出什么行为。一种形式是`std::weak_ptr::lock`,它返回一个`std::shared_ptr`,如果`std::weak_ptr`过期这个`std::shared_ptr`为空:
|
||||
```cpp
|
||||
std::shared_ptr<Widget> spw1 = wpw.lock(); // if wpw's expired, spw1 is null
|
||||
std::shared_ptr<Widget> spw1 = wpw.lock(); //如果wpw过期,spw1就为空
|
||||
|
||||
auto spw2 = wpw.lock(); // same as above, but uses auto
|
||||
auto spw2 = wpw.lock(); //同上,但是使用auto
|
||||
```
|
||||
另一种形式是以`std::weak_ptr`为实参构造`std::shared_ptr`。这种情况中,如果`std::weak_ptr`过期,会抛出一个异常:
|
||||
```cpp
|
||||
std::shared_ptr<Widget> spw3(wpw); // if wpw's expired, throw std::bad_weak_ptr
|
||||
std::shared_ptr<Widget> spw3(wpw); //如果wpw过期,抛出std::bad_weak_ptr异常
|
||||
```
|
||||
但是你可能还想知道为什么`std::weak_ptr`就有用了。考虑一个工厂函数,它基于一个UID从只读对象上产出智能指针。根据Item18的描述,工厂函数会返回一个该对象类型的`std::unique_ptr`:
|
||||
但是你可能还想知道为什么`std::weak_ptr`就有用了。考虑一个工厂函数,它基于一个唯一ID从只读对象上产出智能指针。根据[Item18](https://github.com/kelthuzadx/EffectiveModernCppChinese/blob/master/4.SmartPointers/item19.md)的描述,工厂函数会返回一个该对象类型的`std::unique_ptr`:
|
||||
```cpp
|
||||
std::unique_ptr<const Widget> loadWidget(WidgetID id);
|
||||
std::unique_ptr<const Widget> loadWidget(WidgetID id);
|
||||
```
|
||||
如果调用`loadWidget`是一个昂贵的操作(比如它操作文件或者数据库I/O)并且对于ID来重复使用很常见,一个合理的优化是再写一个函数除了完成`loadWidget`做的事情之外再缓存它的结果。当请求获取一个Widget时阻塞在缓存操作上这本身也会导致性能问题,所以另一个合理的优化可以是当Widget不再使用的时候销毁它的缓存。
|
||||
如果调用`loadWidget`是一个昂贵的操作(比如它操作文件或者数据库I/O)并且重复使用ID很常见,一个合理的优化是再写一个函数除了完成`loadWidget`做的事情之外再缓存它的结果。当每个请求获取的`Widget`阻塞了缓存也会导致本身性能问题,所以另一个合理的优化可以是当`Widget`不再使用的时候销毁它的缓存。
|
||||
|
||||
对于可缓存的工厂函数,返回`std::unique_ptr`不是好的选择。调用者应该接收缓存对象的智能指针,调用者也应该确定这些对象的生命周期,但是缓存本身也需要一个指针指向它所缓的对象。缓存对象的指针需要知道它是否已经悬空,因为当工厂客户端使用完工厂产生的对象后,对象将被销毁,关联的缓存条目会悬空。所以缓存应该使用`std::weak_ptr`,这可以知道是否已经悬空。这意味着工厂函数返回值类型应该是`std::shared_ptr`,因为只有当对象的生命周期由`std::shared_ptr`管理时,`std::weak_ptr`才能检测到悬空。
|
||||
对于可缓存的工厂函数,返回`std::unique_ptr`不是好的选择。调用者应该接收缓存对象的智能指针,调用者也应该确定这些对象的生命周期,但是缓存本身也需要一个指针指向它所缓存的对象。缓存对象的指针需要知道它是否已经悬空,因为当工厂客户端使用完工厂产生的对象后,对象将被销毁,关联的缓存条目会悬空。所以缓存应该使用`std::weak_ptr`,这可以知道是否已经悬空。这意味着工厂函数返回值类型应该是`std::shared_ptr`,因为只有当对象的生命周期由`std::shared_ptr`管理时,`std::weak_ptr`才能检测到悬空。
|
||||
|
||||
下面是一个临时凑合的`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)
|
||||
if (!objPtr) { // if not in cache
|
||||
objPtr = loadWidget(id); // load it
|
||||
cache[id] = objPtr; // cache it
|
||||
}
|
||||
return objPtr;
|
||||
static std::unordered_map<WidgetID,
|
||||
std::weak_ptr<const Widget>> cache;
|
||||
//译者注:这里std::weak_ptr<const Widget>是高亮
|
||||
auto objPtr = cache[id].lock(); //objPtr是去缓存对象的
|
||||
//std::shared_ptr(或
|
||||
//当对象不在缓存中时为null)
|
||||
|
||||
if (!objPtr) { //如果不在缓存中
|
||||
objPtr = loadWidget(id); //加载它
|
||||
cache[id] = objPtr; //缓存它
|
||||
}
|
||||
return objPtr;
|
||||
}
|
||||
```
|
||||
|
||||
这个实现使用了C++11的hash表容器`std::unordered_map`,但是需要的`WidgetID`哈希和相等性比较函数在这里没有展示。
|
||||
|
||||
`fastLoadWidget`的实现忽略了以下事实:cache可能会累积过期的`std::weak_ptr`(对应已经销毁的`Widget`)。可以改进实现方式,但不要花时间在不会引起对`std :: weak_ptr`的深入了解的问题上,让我们考虑第二个用例:观察者设计模式。此模式的主要组件是subjects(状态可能会更改的对象)和observers(状态发生更改时要通知的对象)。在大多数实现中,每个subject都包含一个数据成员,该成员持有指向其observer的指针。这使subject很容易发布状态更改通知。subject对控制observers的生命周期(例如,当它们被销毁时)没有兴趣,但是subject对确保observers被销毁时,不会访问它具有极大的兴趣 。一个合理的设计是每个subject持有其observers的`std::weak_ptr`,因此可以在使用前检查是否已经悬空。
|
||||
`fastLoadWidget`的实现忽略了以下事实:缓存可能会累积过期的`std::weak_ptr`,这些指针对应了不再使用的`Widget`(也已经被销毁了)。其实可以改进实现方式,但是花时间在这个问题上不会让我们对`std::weak_ptr`有更深入的理解,让我们考虑第二个用例:观察者设计模式(Observer design pattern)。此模式的主要组件是subjects(状态可能会更改的对象)和observers(状态发生更改时要通知的对象)。在大多数实现中,每个subject都包含一个数据成员,该成员持有指向其observers的指针。这使subjects很容易发布状态更改通知。subjects对控制observers的生命周期(即它们什么时候被销毁)没有兴趣,但是subjects对确保另一件事具有极大的兴趣,那事就是一个observer被销毁时,不再尝试访问它。一个合理的设计是每个subject持有一个`std::weak_ptr`s容器指向observers,因此可以在使用前检查是否已经悬空。
|
||||
|
||||
作为最后一个使用`std::weak_ptr`的例子,考虑一个持有三个对象A、B、C的数据结构,A和C共享B的所有权,因此持有`std::shared_ptr`:
|
||||
作为最后一个使用`std::weak_ptr`的例子,考虑一个持有三个对象`A`、`B`、`C`的数据结构,`A`和`C`共享`B`的所有权,因此持有`std::shared_ptr`:
|
||||
|
||||

|
||||

|
||||
|
||||
假定从B指向A的指针也很有用。应该使用哪种指针?
|
||||
|
||||

|
||||

|
||||
|
||||
有三种选择:
|
||||
|
||||
- **原始指针**。使用这种方法,如果A被销毁,但是C继续指向B,B就会有一个指向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的引用计数,因此不会导致无法销毁。
|
||||
- **原始指针**。使用这种方法,如果`A`被销毁,但是`C`继续指向`B`,`B`就会有一个指向`A`的悬空指针。而且`B`不知道指针已经悬空,所以`B`可能会继续访问,就会导致未定义行为。
|
||||
- **`std::shared_ptr`**。这种设计,`A`和`B`都互相持有对方的`std::shared_ptr`,导致的`std::shared_ptr`环状结构(`A`指向`B`,`B`指向`A`)阻止`A`和`B`的销毁。甚至`A`和`B`无法从其他数据结构访问了(比如,`C`不再指向`B`),每个的引用计数都还是1。如果发生了这种情况,`A`和`B`都被泄漏:程序无法访问它们,但是资源并没有被回收。
|
||||
- **`std::weak_ptr`**。这避免了上述两个问题。如果`A`被销毁,`B`指向它的指针悬空,但是`B`可以检测到这件事。尤其是,尽管`A`和`B`互相指向对方,`B`的指针不会影响`A`的引用计数,因此在没有`std::shared_ptr`指向`A`时不会导致`A`无法被销毁。
|
||||
|
||||
使用`std::weak_ptr`显然是这些选择中最好的。但是,需要注意使用`std::weak_ptr`打破`std::shared_ptr`循环并不常见。在严格分层的数据结构比如树,子节点只被父节点持有。当父节点被销毁时,子节点就被销毁。从父到子的链接关系可以使用`std::unique_ptr`很好的表征。从子到父的反向连接可以使用原始指针安全实现,因此子节点的生命周期肯定短于父节点。因此子节点解引用一个悬垂的父节点指针是没有问题的。
|
||||
使用`std::weak_ptr`显然是这些选择中最好的。但是,需要注意使用`std::weak_ptr`打破`std::shared_ptr`循环并不常见。在严格分层的数据结构比如树中,子节点只被父节点持有。当父节点被销毁时,子节点就被销毁。从父到子的链接关系可以使用`std::unique_ptr`很好的表征。从子到父的反向连接可以使用原始指针安全实现,因为子节点的生命周期肯定短于父节点。因此没有子节点解引用一个悬垂的父节点指针这样的风险。
|
||||
|
||||
当然,不是所有的使用指针的数据结构都是严格分层的,所以当发生这种情况时,比如上面所述cache和观察者情况,知道`std::weak_ptr`随时待命也是不错的。
|
||||
当然,不是所有的使用指针的数据结构都是严格分层的,所以当发生这种情况时,比如上面所述缓存和观察者列表的实现之类的,知道`std::weak_ptr`随时待命也是不错的。
|
||||
|
||||
从效率角度来看,`std::weak_ptr`与`std::shared_ptr`基本相同。两者的大小是相同的,使用相同的控制块(参见Item 19),构造、析构、赋值操作涉及引用计数的原子操作。这可能让你感到惊讶,因为本Item开篇就提到`std::weak_ptr`不影响引用计数。我写的是`std::weak_ptr`不参与对象的*共享所有权*,因此不影响指向对象的引用计数。实际上在控制块中还是有第二个引用计数,`std::weak_ptr`操作的是第二个引用计数。想了解细节的话,继续看Item 21吧。
|
||||
从效率角度来看,`std::weak_ptr`与`std::shared_ptr`基本相同。两者的大小是相同的,使用相同的控制块(参见[Item19](https://github.com/kelthuzadx/EffectiveModernCppChinese/blob/master/4.SmartPointers/item19.md)),构造、析构、赋值操作涉及引用计数的原子操作。这可能让你感到惊讶,因为本条款开篇就提到`std::weak_ptr`不影响引用计数。我写的是`std::weak_ptr`不参与对象的**共享所有权**,因此不影响**指向对象的引用计数**。实际上在控制块中还是有第二个引用计数,`std::weak_ptr`操作的是第二个引用计数。想了解细节的话,继续看[Item21](https://github.com/kelthuzadx/EffectiveModernCppChinese/blob/master/4.SmartPointers/item21.md)吧。
|
||||
|
||||
### 记住
|
||||
**请记住:**
|
||||
|
||||
- 用`std::weak_ptr`替代可能会悬空的`std::shared_ptr`。
|
||||
- `std::weak_ptr`的潜在使用场景包括:caching、observer lists、打破`std::shared_ptr`指向循环。
|
||||
- `std::weak_ptr`的潜在使用场景包括:缓存、观察者列表、打破`std::shared_ptr`环状结构。
|
||||
|
@ -1,4 +1,6 @@
|
||||
## Item 21:优先考虑使用std::make_unique和std::make_shared而非new
|
||||
## 条款二十一:优先考虑使用`std::make_unique`和`std::make_shared`而非`new`
|
||||
|
||||
**Item 21: Prefer `std::make_unique` and `std::make_shared` to direct use of `new`**
|
||||
|
||||
让我们先对`std::make_unique`和`std::make_shared`做个铺垫。`std::make_shared`是C++11标准的一部分,但很可惜的是,`std::make_unique`不是。它从C++14开始加入标准库。如果你在使用C++11,不用担心,一个基础版本的`std::make_unique`是很容易自己写出的,如下:
|
||||
|
||||
@ -10,203 +12,212 @@ std::unique_ptr<T> make_unique(Ts&&... params)
|
||||
}
|
||||
```
|
||||
|
||||
正如你看到的,`make_unique`只是将它的参数完美转发到所要创建的对象的构造函数,从新产生的原始指针里面构造出`std::unique_ptr`,并返回这个`std::unique_ptr`。这种形式的函数不支持数组和自定义析构,但它给出了一个示范:只需一点努力就能写出你想要的`make_uniqe`函数。需要记住的是,不要把它放到std命名空间中,因为你可能并不希望在升级厂家编译器到符合C++14标准的时候产生冲突。
|
||||
正如你看到的,`make_unique`只是将它的参数完美转发到所要创建的对象的构造函数,从`new`产生的原始指针里面构造出`std::unique_ptr`,并返回这个`std::unique_ptr`。这种形式的函数不支持数组和自定义析构(见[Item18](https://github.com/kelthuzadx/EffectiveModernCppChinese/blob/master/4.SmartPointers/item18.md)),但它给出了一个示范:只需一点努力就能写出你想要的`make_unique`函数。(要想实现一个特性完备的`make_unique`,就去找提供这个的标准化文件吧,然后拷贝那个实现。你想要的这个文件是N3656,是Stephan T. Lavavej写于2013-04-18的文档。)需要记住的是,不要把它放到`std`命名空间中,因为你可能并不希望看到升级C++14标准库的时候你放进`std`命名空间的内容和编译器供应商提供的`std`命名空间的内容发生冲突。
|
||||
|
||||
`std::make_unique`和`std::make_shared`有三个make functions中的两个:接收抽象参数,完美转发到构造函数去动态分配一个对象,然后返回这个指向这个对象的指针。第三个make function 是`std::allocate_shared.`它和`std::make_shared`一样,除了第一个参数是用来动态分配内存的对象。
|
||||
`std::make_unique`和`std::make_shared`是三个**`make`函数**中的两个:接收任意的多参数集合,完美转发到构造函数去动态分配一个对象,然后返回这个指向这个对象的指针。第三个`make`函数是`std::allocate_shared`。它行为和`std::make_shared`一样,只不过第一个参数是用来动态分配内存的*allocator*对象。
|
||||
|
||||
即使是对使用和不使用make函数创建智能指针的最简单比较,也揭示了为什么最好使用这些函数的第一个原因。例如:
|
||||
即使是对使用和不使用`make`函数创建智能指针的最简单比较,也揭示了为什么最好使用`make`函数的第一个原因。例如:
|
||||
|
||||
```c++
|
||||
auto upw1(std::make_unique<Widget>()); // with make func
|
||||
std::unique_ptr<Widget> upw2(new Widget); // without make func
|
||||
auto spw1(std::make_shared<Widget>()); // with make func
|
||||
std::shared_ptr<Widget> spw2(new Widget); // without make func
|
||||
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函数
|
||||
```
|
||||
|
||||
我高亮了区别:使用new的版本重复了类型,但是make function的版本没有。(译者注:这里高亮的是Widget,用new的声明语句需要写2遍Widget,make function只需要写一次) 重复写类型和软件工程里面一个关键原则相冲突:应该避免重复代码。源代码中的重复增加了编译的时间,会导致目标代码冗余,并且通常会让代码库使用更加困难。它经常演变成不一致的代码,而代码库中的不一致常常导致bug。此外,打两次字比一次更费力,而且谁不喜欢减少打字负担?
|
||||
我高亮了关键区别:使用`new`的版本重复了类型,但是`make`函数的版本没有。(译者注:这里高亮的是`Widget`,用`new`的声明语句需要写2遍`Widget`,`make`函数只需要写一次。)重复写类型和软件工程里面一个关键原则相冲突:应该避免重复代码。源代码中的重复增加了编译的时间,会导致目标代码冗余,并且通常会让代码库使用更加困难。它经常演变成不一致的代码,而代码库中的不一致常常导致bug。此外,打两次字比一次更费力,而且没人不喜欢少打字吧?
|
||||
|
||||
第二个使用make function的原因和异常安全有段。假设我们有个函数按照某种优先级处理Widget:
|
||||
第二个使用`make`函数的原因和异常安全有关。假设我们有个函数按照某种优先级处理`Widget`:
|
||||
|
||||
```c++
|
||||
void processWidget(std::shared_ptr<Widget> spw, int priority);
|
||||
```
|
||||
|
||||
根据值传递`std::shared_ptr`可能看起来很可疑,但是Item 41解释了,如果processWidget总是复制`std::shared_ptr`(例如,通过将其存储在已处理的Widget的数据结构中),那么这可能是一个可复用的设计选择。
|
||||
值传递`std::shared_ptr`可能看起来很可疑,但是[Item41](https://github.com/kelthuzadx/EffectiveModernCppChinese/blob/master/8.Tweaks/item41.md)解释了,如果`processWidget`总是复制`std::shared_ptr`(例如,通过将其存储在已处理的`Widget`的一个数据结构中),那么这可能是一个合理的设计选择。
|
||||
|
||||
现在假设我们有一个函数来计算相关的优先级
|
||||
|
||||
`int computePriority();`
|
||||
|
||||
并且我们在调用processWidget时使用了new而不是std:: make_shared
|
||||
现在假设我们有一个函数来计算相关的优先级,
|
||||
|
||||
```c++
|
||||
processWidget(std::shared_ptr<Widget>(new Widget), computePriority()); // potential resource leak!
|
||||
int computePriority();
|
||||
```
|
||||
|
||||
如注释所说,这段代码可能在new Widget时发生泄露。为何?调用的代码和被调用的函数都用`std::shared_ptr`s,且`std::shared_ptr`s就是设计出来防止泄露的。它们会在最后一个`std::shared_ptr`销毁时自动释放所指向的内存。如果每个人在每个地方都用`std::shared_ptr`s,这段代码怎么会泄露呢?
|
||||
并且我们在调用`processWidget`时使用了`new`而不是`std::make_shared`:
|
||||
|
||||
答案和编译器将源码转换为目标代码有关。在运行时,一个函数的参数必须先被计算,才能被调用,所以在调用processWidget之前,必须执行以下操作,processWidget才开始执行:
|
||||
```c++
|
||||
processWidget(std::shared_ptr<Widget>(new Widget), //潜在的资源泄漏!
|
||||
computePriority());
|
||||
```
|
||||
|
||||
- 表达式'new Widget'必须计算,例如,一个Widget对象必须在堆上被创建
|
||||
- 负责管理new出来指针的`std::shared_ptr<Widget>`构造函数必须被执行
|
||||
- computePriority()必须运行
|
||||
如注释所说,这段代码可能在`new`一个`Widget`时发生泄漏。为何?调用的代码和被调用的函数都用`std::shared_ptr`s,且`std::shared_ptr`s就是设计出来防止泄漏的。它们会在最后一个`std::shared_ptr`销毁时自动释放所指向的内存。如果每个人在每个地方都用`std::shared_ptr`s,这段代码怎么会泄漏呢?
|
||||
|
||||
编译器不需要按照执行顺序生成代码。“new Widget"必须在`std::shared_ptr`的构造函数被调用前执行,因为new出来的结果作为构造函数的参数,但compute Priority可能在这之前,之后,或者之间执行。也就是说,编译器可能按照这个执行顺序生成代码:
|
||||
答案和编译器将源码转换为目标代码有关。在运行时,一个函数的实参必须先被计算,这个函数再被调用,所以在调用`processWidget`之前,必须执行以下操作,`processWidget`才开始执行:
|
||||
|
||||
1. 执行new Widget
|
||||
2. 执行computePriority
|
||||
- 表达式“`new Widget`”必须计算,例如,一个`Widget`对象必须在堆上被创建
|
||||
- 负责管理`new`出来指针的`std::shared_ptr<Widget>`构造函数必须被执行
|
||||
- `computePriority`必须运行
|
||||
|
||||
编译器不需要按照执行顺序生成代码。“`new Widget`”必须在`std::shared_ptr`的构造函数被调用前执行,因为`new`出来的结果作为构造函数的实参,但`computePriority`可能在这之前,之后,或者**之间**执行。也就是说,编译器可能按照这个执行顺序生成代码:
|
||||
|
||||
1. 执行“`new Widget`”
|
||||
2. 执行`computePriority`
|
||||
3. 运行`std::shared_ptr`构造函数
|
||||
|
||||
如果按照这样生成代码,并且在运行是computePriority产生了异常,那么第一步动态分配的Widget就会泄露。因为它永远都不会被第三步的`std::shared_ptr`所管理了。
|
||||
如果按照这样生成代码,并且在运行时`computePriority`产生了异常,那么第一步动态分配的`Widget`就会泄漏。因为它永远都不会被第三步的`std::shared_ptr`所管理了。
|
||||
|
||||
使用`std::make_shared`可以防止这种问题。调用代码看起来像是这样:
|
||||
|
||||
```c++
|
||||
processWidget(std::make_shared<Widget>(), computePriority());
|
||||
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`中。如果`computePriority`产生一个异常,那么`std::shared_ptr`析构函数将确保管理的`Widget`被销毁。如果首先调用`computePriority`并产生一个异常,那么`std::make_shared`将不会被调用,因此也就不需要担心动态分配`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`(而不是`new`)同样重要。
|
||||
|
||||
`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分配一次内存,为控制块分配再分配一次内存。
|
||||
显然,这段代码需要进行内存分配,但它实际上执行了两次。[Item19](https://github.com/kelthuzadx/EffectiveModernCppChinese/blob/master/4.SmartPointers/item19.md)解释了每个`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`代替:
|
||||
|
||||
```c++
|
||||
auto spw = std::make_shared<Widget>();
|
||||
```
|
||||
|
||||
一次分配足矣。这是因为`std::make_shared`分配一块内存,同时容纳了`Widget`对象和控制块。这种优化减少了程序的静态大小,因为代码只包含一个内存分配调用,并且它提高了可执行代码的速度,因为内存只分配一次。此外,使用`std::make_shared`避免了对控制块中的某些簿记信息的需要,潜在地减少了程序的总内存占用。
|
||||
|
||||
对于`std::make_shared`的效率分析同样适用于`std::allocate_shared`,因此`std::make_shared`的性能优势也扩展到了该函数。
|
||||
|
||||
更倾向于使用函数而不是直接使用new的争论非常激烈。尽管它们在软件工程、异常安全和效率方面具有优势,但本item的意见是,更倾向于使用make函数,而不是完全依赖于它们。这是因为有些情况下它们不能或不应该被使用。
|
||||
更倾向于使用`make`函数而不是直接使用`new`的争论非常激烈。尽管它们在软件工程、异常安全和效率方面具有优势,但本条款的建议是,更**倾向于**使用`make`函数,而不是完全依赖于它们。这是因为有些情况下它们不能或不应该被使用。
|
||||
|
||||
例如,没有make函数允许指定定制的析构(见item18和19),但是`std::unique_ptr`和`std::shared_ptr`有构造函数这么做。给Widget自定义一个析构:
|
||||
例如,`make`函数都不允许指定自定义删除器(见[Item18](https://github.com/kelthuzadx/EffectiveModernCppChinese/blob/master/4.SmartPointers/item18.md)和[19](https://github.com/kelthuzadx/EffectiveModernCppChinese/blob/master/4.SmartPointers/item19.md)),但是`std::unique_ptr`和`std::shared_ptr`有构造函数这么做。有个`Widget`的自定义删除器:
|
||||
```cpp
|
||||
auto widgetDeleter = [](Widget*){...};
|
||||
auto widgetDeleter = [](Widget* pw) { … };
|
||||
```
|
||||
使用new创建智能指针非常简单:
|
||||
创建一个使用它的智能指针只能直接使用`new`:
|
||||
```cpp
|
||||
std::unique_ptr<Widget, decltype(widgetDeleter)>
|
||||
upw(new Widget, widgetDeleter);
|
||||
upw(new Widget, widgetDeleter);
|
||||
|
||||
std::shared_ptr<Widget> spw(new Widget, widgetDeleter);
|
||||
```
|
||||
对于make函数,没有办法做同样的事情。
|
||||
对于`make`函数,没有办法做同样的事情。
|
||||
|
||||
`make`函数第二个限制来自于其实现中的语法细节。[Item7](https://github.com/kelthuzadx/EffectiveModernCppChinese/blob/master/3.MovingToModernCpp/item7.md)解释了,当构造函数重载,有使用`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函数。
|
||||
好消息是这并非不确定:两种调用都创建了10个元素,每个值为20的`std::vector`。这意味着在`make`函数中,完美转发使用小括号,而不是花括号。坏消息是如果你想用花括号初始化指向的对象,你必须直接使用`new`。使用`make`函数会需要能够完美转发花括号初始化的能力,但是,正如[Item30](https://github.com/kelthuzadx/EffectiveModernCppChinese/blob/master/5.RRefMovSemPerfForw/item30.md)所说,花括号初始化无法完美转发。但是,[Item30](https://github.com/kelthuzadx/EffectiveModernCppChinese/blob/master/5.RRefMovSemPerfForw/item30.md)介绍了一个变通的方法:使用`auto`类型推导从花括号初始化创建`std::initializer_list`对象(见[Item2](https://github.com/kelthuzadx/EffectiveModernCppChinese/blob/master/1.DeducingTypes/item2.md)),然后将`auto`创建的对象传递给`make`函数。
|
||||
|
||||
```cpp
|
||||
// create std::initializer_list
|
||||
//创建std::initializer_list
|
||||
auto initList = { 10, 20 };
|
||||
// create std::vector using std::initializer_list ctor
|
||||
//使用std::initializer_list为形参的构造函数创建std::vector
|
||||
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`)和释放(通过自定义删除器)的支持,因为`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_ptr`s指向控制块,但控制块还有第二个计数,记录多少个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_ptr`s指向控制块。第二个引用计数就是*weak count*。(实际上,*weak count*的值不总是等于指向控制块的`std::weak_ptr`的数目,因为库的实现者找到一些方法在*weak count*中添加附加信息,促进更好的代码产生。为了本条款的目的,我们会忽略这一点,假定*weak count*的值等于指向控制块的`std::weak_ptr`的数目。)当一个`std::weak_ptr`检测它是否过期时(见[Item19](https://github.com/kelthuzadx/EffectiveModernCppChinese/blob/master/4.SmartPointers/item19.md)),它会检测指向的控制块中的引用计数(而不是*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_ptr`s引用一个控制块(即*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 { … };
|
||||
|
||||
// 通过std::make_shared创建一个大对象
|
||||
auto pBigObj = std::make_shared<ReallyBigType>();
|
||||
auto pBigObj = //通过std::make_shared
|
||||
std::make_shared<ReallyBigType>(); //创建一个大对象
|
||||
|
||||
… // 创建 std::shared_ptrs 和 std::weak_ptrs
|
||||
// 指向这个对象,使用它们
|
||||
… //创建std::shared_ptrs和std::weak_ptrs
|
||||
//指向这个对象,使用它们
|
||||
|
||||
… // 最后一个 std::shared_ptr 在这销毁,
|
||||
// 但 std::weak_ptrs 还在
|
||||
… //最后一个std::shared_ptr在这销毁,
|
||||
//但std::weak_ptrs还在
|
||||
|
||||
… // 在这个阶段,原来分配给大对象的内存还分配着
|
||||
… //在这个阶段,原来分配给大对象的内存还分配着
|
||||
|
||||
… // 最后一个std::weak_ptr在这里销毁;
|
||||
// 控制块和对象的内存被释放
|
||||
… //最后一个std::weak_ptr在这里销毁;
|
||||
//控制块和对象的内存被释放
|
||||
```
|
||||
|
||||
直接只用new,一旦最后一个`std::shared_ptr`被销毁,ReallyBigType对象的内存就会被释放:
|
||||
直接只用`new`,一旦最后一个`std::shared_ptr`被销毁,`ReallyBigType`对象的内存就会被释放:
|
||||
|
||||
```c++
|
||||
class ReallyBigType { … };
|
||||
class ReallyBigType { … }; //和之前一样
|
||||
|
||||
//通过new创建特大对象
|
||||
std::shared_ptr<ReallyBigType> pBigObj(new ReallyBigType);
|
||||
//通过new创建大对象
|
||||
|
||||
… // 像之前一样,创建 std::shared_ptrs 和 std::weak_ptrs
|
||||
// 指向这个对象,使用它们
|
||||
… //像之前一样,创建std::shared_ptrs和std::weak_ptrs
|
||||
//指向这个对象,使用它们
|
||||
|
||||
… // 最后一个 std::shared_ptr 在这销毁,
|
||||
// 但 std::weak_ptrs 还在
|
||||
|
||||
// memory for object is deallocated
|
||||
… //最后一个std::shared_ptr在这销毁,
|
||||
//但std::weak_ptrs还在;
|
||||
//对象的内存被释放
|
||||
|
||||
… // 在这阶段,只有控制块的内存仍然保持分配
|
||||
… //在这阶段,只有控制块的内存仍然保持分配
|
||||
|
||||
… // 最后一个std::weak_ptr在这里销毁;
|
||||
// 控制块内存被释放
|
||||
… //最后一个std::weak_ptr在这里销毁;
|
||||
//控制块内存被释放
|
||||
```
|
||||
|
||||
如果你发现自己处于不可能或不合适使用`std::make_shared`的情况下,你将想要保证自己不受我们之前看到的异常安全问题的影响。最好的方法是确保在直接使用new时,在一个不做其他事情的语句中,立即将结果传递到智能指针构造函数。这可以防止编译器生成的代码在使用new和调用管理新对象的智能指针的构造函数之间发生异常。
|
||||
如果你发现自己处于不可能或不合适使用`std::make_shared`的情况下,你将想要保证自己不受我们之前看到的异常安全问题的影响。最好的方法是确保在直接使用`new`时,在**一个不做其他事情的语句中**,立即将结果传递到智能指针构造函数。这可以防止编译器生成的代码在使用`new`和调用管理`new`出来对象的智能指针的构造函数之间发生异常。
|
||||
|
||||
例如,考虑我们前面讨论过的processWidget函数,对其非异常安全调用的一个小修改。这一次,我们将指定一个自定义删除器:
|
||||
例如,考虑我们前面讨论过的`processWidget`函数,对其非异常安全调用的一个小修改。这一次,我们将指定一个自定义删除器:
|
||||
```c++
|
||||
void processWidget(std::shared_ptr<Widget> spw, int priority);
|
||||
void cusDel(Widget *ptr); // 自定义删除器
|
||||
void processWidget(std::shared_ptr<Widget> spw, //和之前一样
|
||||
int priority);
|
||||
void cusDel(Widget *ptr); //自定义删除器
|
||||
```
|
||||
|
||||
这是非异常安全调用:
|
||||
```c++
|
||||
//和之前一样,潜在的内存泄露
|
||||
processWidget(
|
||||
std::shared_ptr<Widget>(new Widget, cusDel),
|
||||
processWidget( //和之前一样,
|
||||
std::shared_ptr<Widget>(new Widget, cusDel), //潜在的内存泄漏!
|
||||
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
|
||||
一个小小的性能问题是,在非异常安全调用中,我们将一个右值传递给`processWidget`:
|
||||
```c++
|
||||
processWidget(
|
||||
std::shared_ptr<Widget>(new Widget, cusDel), // arg is rvalue
|
||||
std::shared_ptr<Widget>(new Widget, cusDel), //实参是一个右值
|
||||
computePriority()
|
||||
);
|
||||
```
|
||||
但是在异常安全调用中,我们传递了左值
|
||||
但是在异常安全调用中,我们传递了左值:
|
||||
```c++
|
||||
processWidget(spw, computePriority()); //spw是左值
|
||||
processWidget(spw, computePriority()); //实参是左值
|
||||
```
|
||||
因为processWidget的`std::shared_ptr`参数是传值,传右值给构造函数只需要move,而传递左值需要拷贝。对`std::shared_ptr`而言,这种区别是有意义的,因为拷贝`std::shared_ptr`需要对引用计数原子加,move则不需要对引用计数有操作。为了使异常安全代码达到异常不安全代码的性能水平,我们需要用std::move将spw转换为右值.
|
||||
因为`processWidget`的`std::shared_ptr`形参是传值,从右值构造只需要移动,而传递左值构造需要拷贝。对`std::shared_ptr`而言,这种区别是有意义的,因为拷贝`std::shared_ptr`需要对引用计数原子递增,移动则不需要对引用计数有操作。为了使异常安全代码达到非异常安全代码的性能水平,我们需要用`std::move`将`spw`转换为右值(见[Item23](https://github.com/kelthuzadx/EffectiveModernCppChinese/blob/master/5.RRefMovSemPerfForw/item23.md)):
|
||||
```c++
|
||||
processWidget(std::move(spw), computePriority());
|
||||
processWidget(std::move(spw), computePriority()); //高效且异常安全
|
||||
```
|
||||
这很有趣,也值得了解,但通常是无关紧要的,因为您很少有理由不使用make函数。除非你有令人信服的理由这样做,否则你应该使用make函数。
|
||||
这很有趣,也值得了解,但通常是无关紧要的,因为您很少有理由不使用`make`函数。除非你有令人信服的理由这样做,否则你应该使用`make`函数。
|
||||
|
||||
**记住**:
|
||||
- 和直接使用new相比,make函数消除了代码重复,提高了异常安全性。对于`std::make_shared`和`std::allocate_shared`,生成的代码更小更快。
|
||||
- 不适合使用make函数的情况包括需要指定自定义删除器和希望用大括号初始化
|
||||
- 对于`std::shared_ptr`s, make函数可能不被建议的其他情况包括
|
||||
(1)有自定义内存管理的类和
|
||||
(2)特别关注内存的系统,非常大的对象,以及`std::weak_ptr`s比对应的`std::shared_ptr`s活得更久
|
||||
**请记住:**
|
||||
- 和直接使用`new`相比,`make`函数消除了代码重复,提高了异常安全性。对于`std::make_shared`和`std::allocate_shared`,生成的代码更小更快。
|
||||
- 不适合使用`make`函数的情况包括需要指定自定义删除器和希望用花括号初始化。
|
||||
- 对于`std::shared_ptr`s,其他不建议使用`make`函数的情况包括(1)有自定义内存管理的类;(2)特别关注内存的系统,非常大的对象,以及`std::weak_ptr`s比对应的`std::shared_ptr`s活得更久。
|
||||
|
@ -1,97 +1,97 @@
|
||||
## 当使用Pimpl惯用法,请在实现文件中定义特殊成员函数
|
||||
## 条款二十二:当使用Pimpl惯用法,请在实现文件中定义特殊成员函数
|
||||
|
||||
如果你曾经与过多的编译次数斗争过,你会对`Pimpl`(Pointer to implementation)惯用法很熟悉。 凭借这样一种技巧,你可以将一个**类数据成员**替换成一个指向包含具体实现的类或结构体的指针, 并将放在主类(primary class)的数据成员们移动到实现类去(implementation class), 而这些数据成员的访问将通过指针间接访问。 举个例子,假如有一个类`Widget`看起来如下:
|
||||
**Item 22: When using the Pimpl Idiom, define special member functions in the implementation file**
|
||||
|
||||
如果你曾经与过多的编译次数斗争过,你会对**Pimpl**(*pointer to implementation*)**惯用法**很熟悉。 凭借这样一种技巧,你可以将类数据成员替换成一个指向包含具体实现的类(或结构体)的指针,并将放在主类(primary class)的数据成员们移动到实现类(implementation class)去,而这些数据成员的访问将通过指针间接访问。 举个例子,假如有一个类`Widget`看起来如下:
|
||||
|
||||
```cpp
|
||||
class Widget() //定义在头文件`widget.h`
|
||||
{
|
||||
class Widget() { //定义在头文件“widget.h”
|
||||
public:
|
||||
Widget();
|
||||
...
|
||||
…
|
||||
private:
|
||||
std::string name;
|
||||
std::vector<double> data;
|
||||
Gadget g1, g2, g3; //Gadget是用户自定义的类型
|
||||
}
|
||||
Gadget g1, g2, g3; //Gadget是用户自定义的类型
|
||||
};
|
||||
```
|
||||
|
||||
因为类`Widget`的数据成员包含有类型`std::string`,`std::vector`和`Gadget`, 定义有这些类型的头文件在类`Widget`编译的时候,必须被包含进来,这意味着类`Widget`的使用者必须要`#include <string>,<vector>`以及`gadget.h`。 这些头文件将会增加类`Widget`使用者的编译时间,并且让这些使用者依赖于这些头文件。 如果一个头文件的内容变了,类`Widget`使用者也必须要重新编译。 标准库文件`<string>`和`<vector>`不是很常变,但是`gadget.h`可能会经常修订。
|
||||
因为类`Widget`的数据成员包含有类型`std::string`,`std::vector`和`Gadget`, 定义有这些类型的头文件在类`Widget`编译的时候,必须被包含进来,这意味着类`Widget`的使用者必须要`#include <string>`,`<vector>`以及`gadget.h`。 这些头文件将会增加类`Widget`使用者的编译时间,并且让这些使用者依赖于这些头文件。 如果一个头文件的内容变了,类`Widget`使用者也必须要重新编译。 标准库文件`<string>`和`<vector>`不是很常变,但是`gadget.h`可能会经常修订。
|
||||
|
||||
在C++98中使用`Pimpl`惯用法,可以把`Widget`的数据成员替换成一个原始指针(raw pointer),指向一个已经被声明过却还未被定义的类,如下:
|
||||
在C++98中使用Pimpl惯用法,可以把`Widget`的数据成员替换成一个原始指针,指向一个已经被声明过却还未被定义的结构体,如下:
|
||||
|
||||
```cpp
|
||||
class Widget //仍然在"Widget.h"中
|
||||
class Widget //仍然在“widget.h”中
|
||||
{
|
||||
public:
|
||||
Widget();
|
||||
~Widget(); //析构函数在后面会分析
|
||||
...
|
||||
~Widget(); //析构函数在后面会分析
|
||||
…
|
||||
|
||||
private:
|
||||
struct Impl; //声明一个 实现结构体
|
||||
Impl *pImpl; //以及指向它的指针
|
||||
}
|
||||
private:
|
||||
struct Impl; //声明一个 实现结构体
|
||||
Impl *pImpl; //以及指向它的指针
|
||||
};
|
||||
```
|
||||
|
||||
因为类`Widget`不再提到类型`std:::string`,`std::vector`以及`Gadget`,`Widget`的使用者不再需要为了这些类型而引入头文件。 这可以加速编译,并且意味着,如果这些头文件中有所变动,`Widget` 的使用者不会受到影响。
|
||||
因为类`Widget`不再提到类型`std::string`,`std::vector`以及`Gadget`,`Widget`的使用者不再需要为了这些类型而引入头文件。 这可以加速编译,并且意味着,如果这些头文件中有所变动,`Widget`的使用者不会受到影响。
|
||||
|
||||
一个已经被声明,却还未被实现的类型,被称为**未完成类型**(incomplete type)。 `Widget::Impl`就是这种类型。 你能对一个未完成类型做的事很少,但是声明一个指向它指针是可以的。 `Pimpl`手法利用了这一点。
|
||||
一个已经被声明,却还未被实现的类型,被称为**未完成类型**(*incomplete type*)。 `Widget::Impl`就是这种类型。 你能对一个未完成类型做的事很少,但是声明一个指向它的指针是可以的。Pimpl惯用法利用了这一点。
|
||||
|
||||
`Pimpl`惯用法的第一步,是声明一个数据成员,它是个指针,指向一个未完成类型。 第二步是动态分配(dynamic allocation)和回收一个对象,该对象包含那些以前在原来的类中的数据成员。 内存分配和回收的代码都写在**实现文件**(implementation file)里,比如,对于类`Widget`而言,写在`Widget.cpp`里:
|
||||
Pimpl惯用法的第一步,是声明一个数据成员,它是个指针,指向一个未完成类型。 第二步是动态分配和回收一个对象,该对象包含那些以前在原来的类中的数据成员。 内存分配和回收的代码都写在实现文件里,比如,对于类`Widget`而言,写在`Widget.cpp`里:
|
||||
|
||||
```cpp
|
||||
#include "widget.h" //以下代码均在实现文件 widget.cpp里
|
||||
#include "widget.h" //以下代码均在实现文件“widget.cpp”里
|
||||
#include "gadget.h"
|
||||
#include <string>
|
||||
#include <vector>
|
||||
struct Widget::Impl //之前在Widget中声明的Widget::Impl类型的定义
|
||||
{
|
||||
std::string name;
|
||||
|
||||
struct Widget::Impl { //含有之前在Widget中的数据成员的
|
||||
std::string name; //Widget::Impl类型的定义
|
||||
std::vector<double> data;
|
||||
Gadget g1,g2,g3;
|
||||
}
|
||||
};
|
||||
|
||||
Widget::Widget() //为此Widget对象分配数据成员
|
||||
Widget::Widget() //为此Widget对象分配数据成员
|
||||
: pImpl(new Impl)
|
||||
{}
|
||||
|
||||
Widget::~Widget()
|
||||
{delete pImpl;} //销毁数据成员
|
||||
Widget::~Widget() //销毁数据成员
|
||||
{ delete pImpl; }
|
||||
```
|
||||
|
||||
在这里我把`#include`命令写出来是为了明确一点,对于头文件`std::string`,`std::vector`和`Gadget`的整体依赖依然存在。 然而,这些依赖从头文件`widget.h`(它被所有`Widget`类的使用者包含,并且对他们可见)移动到了`widget.cpp`(该文件只被`Widget`类的实现者包含,并只对它可见)。 我高亮了其中动态分配和回收`Impl`对象的部分(markdown高亮不了,实际是`new`和`delete`两部分——译者注)。这就是为什么我们需要`Widget`的析构函数——我们需要回收该对象。
|
||||
在这里我把`#include`命令写出来是为了明确一点,对于`std::string`,`std::vector`和`Gadget`的头文件的整体依赖依然存在。 然而,这些依赖从头文件`widget.h`(它被所有`Widget`类的使用者包含,并且对他们可见)移动到了`widget.cpp`(该文件只被`Widget`类的实现者包含,并只对他可见)。 我高亮了其中动态分配和回收`Impl`对象的部分(译者注:markdown高亮不了,实际高亮的是`new Impl`和`delete pImpl;`两个语句)。这就是为什么我们需要`Widget`的析构函数——我们需要`Widget`被销毁时回收该对象。
|
||||
|
||||
但是,我展示给你们看的是一段C++98的代码,散发着一股已经过去了几千年的腐朽气息。 它使用了原始指针,原始的`new`和原始的`delete`,一切都让它如此的...原始。这一章建立在“智能指针比原始指针更好”的主题上,并且,如果我们想要的只是在类`Widget`的构造函数动态分配`Widget::impl`对象,在`Widget`对象销毁时一并销毁它, `std::unique_ptr`(见Item 18)是最合适的工具。 在头文件中用`std::unique_ptr`替代原始指针,就有了如下代码:
|
||||
但是,我展示给你们看的是一段C++98的代码,散发着一股已经过去了几千年的腐朽气息。 它使用了原始指针,原始的`new`和原始的`delete`,一切都让它如此的...原始。这一章建立在“智能指针比原始指针更好”的主题上,并且,如果我们想要的只是在类`Widget`的构造函数动态分配`Widget::impl`对象,在`Widget`对象销毁时一并销毁它, `std::unique_ptr`(见[Item18](https://github.com/kelthuzadx/EffectiveModernCppChinese/blob/master/4.SmartPointers/item18.md))是最合适的工具。在头文件中用`std::unique_ptr`替代原始指针,就有了头文件中如下代码:
|
||||
|
||||
```cpp
|
||||
class Widget //在"Widget.h"中
|
||||
{
|
||||
class Widget { //在“widget.h”中
|
||||
public:
|
||||
Widget();
|
||||
...
|
||||
…
|
||||
|
||||
private:
|
||||
struct Impl; //声明一个 实现结构体
|
||||
std::unique_ptr<Impl> pImpl; //使用智能指针而不是原始指针
|
||||
}
|
||||
private:
|
||||
struct Impl;
|
||||
std::unique_ptr<Impl> pImpl; //使用智能指针而不是原始指针
|
||||
};
|
||||
```
|
||||
|
||||
实现文件也可以改成如下:
|
||||
实现文件也可以改成如下:
|
||||
|
||||
```cpp
|
||||
#include "widget.h" //以下代码均在实现文件 widget.cpp里
|
||||
#include "widget.h" //在“widget.cpp”中
|
||||
#include "gadget.h"
|
||||
#include <string>
|
||||
#include <vector>
|
||||
struct Widget::Impl //跟之前一样
|
||||
{
|
||||
|
||||
struct Widget::Impl { //跟之前一样
|
||||
std::string name;
|
||||
std::vector<double> data;
|
||||
Gadget g1,g2,g3;
|
||||
}
|
||||
};
|
||||
|
||||
Widget::Widget() //根据Item 21, 通过std::make_shared来创建std::unique_ptr
|
||||
: pImpl(std::make_unique<Imple>())
|
||||
Widget::Widget() //根据条款21,通过std::make_unique
|
||||
: pImpl(std::make_unique<Impl>()) //来创建std::unique_ptr
|
||||
{}
|
||||
```
|
||||
|
||||
@ -103,221 +103,182 @@ Widget::Widget() //根据Item 21, 通过std::make_shared来创建std::u
|
||||
```cpp
|
||||
#include "widget.h"
|
||||
|
||||
Wdiget w; //编译出错
|
||||
Widget w; //错误!
|
||||
```
|
||||
|
||||
你所看到的错误信息根据编译器不同会有所不同,但是其文本一般会提到一些**有关于把`sizeof`和`delete`应用到未完成类型`incomplete type`上**的信息。对于未完成类型,使用以上操作是禁止的。
|
||||
你所看到的错误信息根据编译器不同会有所不同,但是其文本一般会提到一些有关于“把`sizeof`或`delete`应用到未完成类型上”的信息。对于未完成类型,使用以上操作是禁止的。
|
||||
|
||||
在`Pimpl`惯用法中使用`std::unique_ptr`会抛出错误,有点惊悚,因为第一`std::unique_ptr`宣称它支持未完成类型,第二`Pimpl`惯用法是`std::unique_ptr`的最常见的用法。 幸运的是,让这段代码能正常运行很简单。 只需要对是什么导致以上代码编译出错有一个基础的认识就可以了。
|
||||
在Pimpl惯用法中使用`std::unique_ptr`会抛出错误,有点惊悚,因为第一`std::unique_ptr`宣称它支持未完成类型,第二Pimpl惯用法是`std::unique_ptr`的最常见的使用情况之一。 幸运的是,让这段代码能正常运行很简单。 只需要对上面出现的问题的原因有一个基础的认识就可以了。
|
||||
|
||||
在对象`w`被析构时,例如离开了作用域(scope),问题出现了。在这个时候,它的析构函数被调用。我们在类的定义里使用了`std::unique_ptr`,所以我们没有声明一个析构函数,因为我们并没有任何代码需要写在里面。根据编译器自动生成的*特殊成员函数*的规则(见 Item 17),编译器会自动为我们生成一个析构函数。 在这个析构函数里,编译器会插入一些代码来调用类`Widget`的数据成员`Pimpl`的析构函数。 `Pimpl`是一个`std::unique_ptr<Widget::Impl>`,也就是说,一个带有默认销毁器(default deleter)的`std::unique_ptr`。 默认销毁器(default deleter)是一个函数,它使用`delete`来销毁内置于`std::unique_ptr`的原始指针。然而,在使用`delete`之前,通常会使默认销毁器使用C++11的特性`static_assert`来确保原始指针指向的类型不是一个未完成类型。 当编译器为`Widget w`的析构生成代码时,它会遇到`static_assert`检查并且失败,这通常是错误信息的来源。 这些错误信息只在对象`w`销毁的地方出现,因为类`Widget`的析构函数,正如其他的编译器生成的特殊成员函数一样,是暗含`inline`属性的。 错误信息自身往往指向对象`w`被创建的那行,因为这行代码明确地构造了这个对象,导致了后面潜在的析构。
|
||||
在对象`w`被析构时(例如离开了作用域),问题出现了。在这个时候,它的析构函数被调用。我们在类的定义里使用了`std::unique_ptr`,所以我们没有声明一个析构函数,因为我们并没有任何代码需要写在里面。根据编译器自动生成的特殊成员函数的规则(见 [Item17](https://github.com/kelthuzadx/EffectiveModernCppChinese/blob/master/3.MovingToModernCpp/item17.md)),编译器会自动为我们生成一个析构函数。 在这个析构函数里,编译器会插入一些代码来调用类`Widget`的数据成员`pImpl`的析构函数。 `pImpl`是一个`std::unique_ptr<Widget::Impl>`,也就是说,一个使用默认删除器的`std::unique_ptr`。 默认删除器是一个函数,它使用`delete`来销毁内置于`std::unique_ptr`的原始指针。然而,在使用`delete`之前,通常会使默认删除器使用C++11的特性`static_assert`来确保原始指针指向的类型不是一个未完成类型。 当编译器为`Widget w`的析构生成代码时,它会遇到`static_assert`检查并且失败,这通常是错误信息的来源。 这些错误信息只在对象`w`销毁的地方出现,因为类`Widget`的析构函数,正如其他的编译器生成的特殊成员函数一样,是暗含`inline`属性的。 错误信息自身往往指向对象`w`被创建的那行,因为这行代码明确地构造了这个对象,导致了后面潜在的析构。
|
||||
|
||||
为了解决这个问题,你只需要确保在编译器生成销毁`std::unique_ptr<Widget::Imple>`的代码之前, `Widget::Impl`已经是一个完成类型(complete type)。 当编译器"看到"它的定义的时候,该类型就成为*完成类型*了。 但是 `Widget::Impl`的定义在`wideget.cpp`里。成功编译的关键,就是,在`widget.cpp`文件内,让编译器在"看到" `Widget`的析构函数实现之前(也即编译器自动插入销毁`std::unique_ptr`的数据成员的位置),先定义`Wdiget::Impl`。
|
||||
为了解决这个问题,你只需要确保在编译器生成销毁`std::unique_ptr<Widget::Impl>`的代码之前, `Widget::Impl`已经是一个完成类型(*complete type*)。 当编译器“看到”它的定义的时候,该类型就成为完成类型了。 但是 `Widget::Impl`的定义在`widget.cpp`里。成功编译的关键,就是在`widget.cpp`文件内,让编译器在“看到” `Widget`的析构函数实现之前(也即编译器插入的,用来销毁`std::unique_ptr`这个数据成员的代码的,那个位置),先定义`Widget::Impl`。
|
||||
|
||||
做出这样的调整很容易。只需要在先在`widget.h`里,只**声明**(declare)类`Widget`的析构函数,却不要在这里**定义**(define)它:
|
||||
做出这样的调整很容易。只需要在先在`widget.h`里,只声明类`Widget`的析构函数,却不要在这里定义它:
|
||||
|
||||
```cpp
|
||||
class Widget { // as before, in "widget.h"
|
||||
class Widget { //跟之前一样,在“widget.h”中
|
||||
public:
|
||||
Widget();
|
||||
~Widget(); // declaration only
|
||||
...
|
||||
Widget();
|
||||
~Widget(); //只有声明语句
|
||||
…
|
||||
|
||||
private: // as before
|
||||
struct Impl;
|
||||
std::unique_ptr<Impl> pImpl;
|
||||
private: //跟之前一样
|
||||
struct Impl;
|
||||
std::unique_ptr<Impl> pImpl;
|
||||
};
|
||||
```
|
||||
|
||||
|
||||
|
||||
在`widget.cpp`文件中,在结构体`Widget::Impl`被定义之后,再定义析构函数:
|
||||
在`widget.cpp`文件中,在结构体`Widget::Impl`被定义之后,再定义析构函数:
|
||||
|
||||
```cpp
|
||||
#include "widget.h" //以下代码均在实现文件 widget.cpp里
|
||||
#include "widget.h" //跟之前一样,在“widget.cpp”中
|
||||
#include "gadget.h"
|
||||
#include <string>
|
||||
#include <vector>
|
||||
struct Widget::Impl //跟之前一样,定义Widget::Impl
|
||||
{
|
||||
|
||||
struct Widget::Impl { //跟之前一样,定义Widget::Impl
|
||||
std::string name;
|
||||
std::vector<double> data;
|
||||
Gadget g1,g2,g3;
|
||||
}
|
||||
|
||||
Widget::Widget() //根据Item 21, 通过std::make_shared来创建std::unique_ptr
|
||||
: pImpl(std::make_unique<Imple>())
|
||||
Widget::Widget() //跟之前一样
|
||||
: pImpl(std::make_unique<Impl>())
|
||||
{}
|
||||
|
||||
Widget::~Widget() //析构函数的定义(译者注:这里高亮)
|
||||
Widget::~Widget() //析构函数的定义(译者注:这里高亮)
|
||||
{}
|
||||
```
|
||||
|
||||
这样就可以了,并且这样增加的代码也最少,但是,如果你想要强调编译器自动生成的析构函数会工作的很好——你声明`Widget`的析构函数的唯一原因,是确保它会在`Widget`的实现文件内(指`widget.cpp`,译者注)被自动生成,你可以把析构函数体直接定义为`=default`:
|
||||
这样就可以了,并且这样增加的代码也最少,但是,如果你想要强调编译器自动生成的析构函数做的没错——你声明`Widget`的析构函数的唯一原因,是确保它会在`Widget`的实现文件内(译者注:指`widget.cpp`)被自动生成,你可以把析构函数体直接定义为“`= default`”:
|
||||
|
||||
```cpp
|
||||
Widget::~Widget() = default; //同上述代码效果一致
|
||||
Widget::~Widget() = default; //同上述代码效果一致
|
||||
```
|
||||
|
||||
使用了`Pimpl`惯用法的类自然适合支持移动操作,因为编译器自动生成的移动操作正合我们所意: 对隐藏的`std::unique_ptr`进行移动。 正如`Item 17`所解释的那样,声明一个类`Widget`的析构函数会阻止编译器生成移动操作,所以如果你想要支持移动操作,你必须自己声明相关的函数。考虑到编译器自动生成的版本能够正常功能,你可能会被诱使着来这样实现:
|
||||
使用了Pimpl惯用法的类自然适合支持移动操作,因为编译器自动生成的移动操作正合我们所意:对其中的`std::unique_ptr`进行移动。 正如[Item17](https://github.com/kelthuzadx/EffectiveModernCppChinese/blob/master/3.MovingToModernCpp/item17.md)所解释的那样,声明一个类`Widget`的析构函数会阻止编译器生成移动操作,所以如果你想要支持移动操作,你必须自己声明相关的函数。考虑到编译器自动生成的版本能够正常功能,你可能会被诱使着来这样实现:
|
||||
|
||||
```cpp
|
||||
class Widget //在"Widget.h"中
|
||||
{
|
||||
class Widget { //仍然在“widget.h”中
|
||||
public:
|
||||
Widget();
|
||||
~Widget();
|
||||
...
|
||||
|
||||
Widget(Widget&& rhs) = default; //思路正确,但代码错误
|
||||
Widget& operator=(Widget&& rhs) = default;
|
||||
Widget(Widget&& rhs) = default; //思路正确,
|
||||
Widget& operator=(Widget&& rhs) = default; //但代码错误
|
||||
…
|
||||
|
||||
|
||||
private:
|
||||
struct Impl; //如上
|
||||
private: //跟之前一样
|
||||
struct Impl;
|
||||
std::unique_ptr<Impl> pImpl;
|
||||
}
|
||||
};
|
||||
```
|
||||
|
||||
这样的做法会导致同样的错误,和之前的声明一个不带析构函数的类的错误一样,并且是因为同样的原因。 编译器生成的移动赋值操作符(move assignment operator),在重新赋值之前,需要先销毁指针`pImpl`指向的对象。然而在`Widget`的头文件里,`pImpl`指针指向的是一个未完成类型。情况和移动构造函数(move constructor)有所不同。 移动构造函数的问题是编译器自动生成的代码里,包含有抛出异常的事件,在这个事件里会生成销毁`pImpl`的代码。然而,销毁`pImpl`需要`Impl`是一个完成类型。
|
||||
这样的做法会导致同样的错误,和之前的声明一个不带析构函数的类的错误一样,并且是因为同样的原因。 编译器生成的移动赋值操作符,在重新赋值之前,需要先销毁指针`pImpl`指向的对象。然而在`Widget`的头文件里,`pImpl`指针指向的是一个未完成类型。移动构造函数的情况有所不同。 移动构造函数的问题是编译器自动生成的代码里,包含有抛出异常的事件,在这个事件里会生成销毁`pImpl`的代码。然而,销毁`pImpl`需要`Impl`是一个完成类型。
|
||||
|
||||
因为这个问题同上面一致,所以解决方案也一样——把移动操作的定义移动到实现文件里:
|
||||
因为这个问题同上面一致,所以解决方案也一样——把移动操作的定义移动到实现文件里:
|
||||
```cpp
|
||||
class Widget //在"Widget.h"中
|
||||
{
|
||||
class Widget { //仍然在“widget.h”中
|
||||
public:
|
||||
Widget();
|
||||
~Widget();
|
||||
...
|
||||
|
||||
Widget(Widget&& rhs); //仅声明
|
||||
Widget(Widget&& rhs); //只有声明
|
||||
Widget& operator=(Widget&& rhs);
|
||||
…
|
||||
|
||||
|
||||
private:
|
||||
struct Impl; //如上
|
||||
private: //跟之前一样
|
||||
struct Impl;
|
||||
std::unique_ptr<Impl> pImpl;
|
||||
}
|
||||
};
|
||||
```
|
||||
|
||||
```cpp
|
||||
#include "widget.h" //以下代码均在实现文件 widget.cpp里
|
||||
#include "gadget.h"
|
||||
#include <string>
|
||||
#include <vector>
|
||||
struct Widget::Impl //跟之前一样,定义Widget::Impl
|
||||
{
|
||||
std::string name;
|
||||
std::vector<double> data;
|
||||
Gadget g1,g2,g3;
|
||||
}
|
||||
#include <string> //跟之前一样,仍然在“widget.cpp”中
|
||||
…
|
||||
|
||||
struct Widget::Impl { … }; //跟之前一样
|
||||
|
||||
Widget::Widget() //根据Item 21, 通过std::make_shared来创建std::unique_ptr
|
||||
: pImpl(std::make_unique<Imple>())
|
||||
Widget::Widget() //跟之前一样
|
||||
: pImpl(std::make_unique<Impl>())
|
||||
{}
|
||||
|
||||
Widget::~Widget() = default;
|
||||
Widget::~Widget() = default; //跟之前一样
|
||||
|
||||
Widget(Widget&& rhs) = default; //在这里定义
|
||||
Widget& operator=(Widget&& rhs) = default;
|
||||
Widget::Widget(Widget&& rhs) = default; //这里定义
|
||||
Widget& Widget::operator=(Widget&& rhs) = default;
|
||||
```
|
||||
|
||||
**`pImpl`惯用法是用来减少类实现者和类使用者之间的编译依赖的一种方法**,但是,从概念而言,使用这种惯用法并不改变这个类的表现。 原来的类`Widget`包含有`std::string`,`std::vector`和`Gadget`数据成员,并且,假设类型`Gadget`,如同`std::string`和`std::vector`一样,允许复制操作,所以类`Widget`支持复制操作也很合理。 我们必须要自己来写这些函数,因为第一,对包含有**只可移动(move-only)**类型,如`std::unique_ptr`的类,编译器不会生成复制操作;第二,即使编译器帮我们生成了,生成的复制操作也只会复制`std::unique_ptr`(也即浅复制(shallow copy)),而实际上我们需要复制指针所指向的对象(也即深复制(deep copy))。
|
||||
Pimpl惯用法是用来减少类的实现和类使用者之间的编译依赖的一种方法,但是,从概念而言,使用这种惯用法并不改变这个类的表现。 原来的类`Widget`包含有`std::string`,`std::vector`和`Gadget`数据成员,并且,假设类型`Gadget`,如同`std::string`和`std::vector`一样,允许复制操作,所以类`Widget`支持复制操作也很合理。 我们必须要自己来写这些函数,因为第一,对包含有只可移动(*move-only*)类型,如`std::unique_ptr`的类,编译器不会生成复制操作;第二,即使编译器帮我们生成了,生成的复制操作也只会复制`std::unique_ptr`(也即浅复制(*shallow copy*)),而实际上我们需要复制指针所指向的对象(也即深复制(*deep copy*))。
|
||||
|
||||
使用我们已经熟悉的方法,我们在头文件里声明函数,而在实现文件里去实现他们:
|
||||
|
||||
```cpp
|
||||
class Widget //在"Widget.h"中
|
||||
{
|
||||
class Widget { //仍然在“widget.h”中
|
||||
public:
|
||||
Widget();
|
||||
~Widget();
|
||||
...
|
||||
…
|
||||
|
||||
Widget(const Widget& rhs); //仅声明
|
||||
Widget(const Widget& rhs); //只有声明
|
||||
Widget& operator=(const Widget& rhs);
|
||||
|
||||
|
||||
private:
|
||||
struct Impl; //如上
|
||||
private: //跟之前一样
|
||||
struct Impl;
|
||||
std::unique_ptr<Impl> pImpl;
|
||||
}
|
||||
};
|
||||
```
|
||||
|
||||
```cpp
|
||||
#include "widget.h" //以下代码均在实现文件 widget.cpp里
|
||||
#include "gadget.h"
|
||||
#include <string>
|
||||
#include <vector>
|
||||
struct Widget::Impl //跟之前一样,定义Widget::Impl
|
||||
{
|
||||
...
|
||||
}
|
||||
#include <string> //跟之前一样,仍然在“widget.cpp”中
|
||||
…
|
||||
|
||||
struct Widget::Impl { … }; //跟之前一样
|
||||
|
||||
Widget::Widget() //根据Item 21, 通过std::make_shared来创建std::unique_ptr
|
||||
: pImpl(std::make_unique<Imple>())
|
||||
Widget::~Widget() = default; //其他函数,跟之前一样
|
||||
|
||||
Widget::Widget(const Widget& rhs) //拷贝构造函数
|
||||
: pImpl(std::make_unique<Impl>(*rhs.pImpl))
|
||||
{}
|
||||
|
||||
Widget::~Widget() = default;
|
||||
...
|
||||
Widget::Widget(const Widget& rhs)
|
||||
:pImpl(std::make_unique<Impl>(*rhs.pImpl))
|
||||
{}
|
||||
|
||||
Widget& Widget::operator=(const Widget& rhs)
|
||||
Widget& Widget::operator=(const Widget& rhs) //拷贝operator=
|
||||
{
|
||||
*pImpl = *rhs.pImpl;
|
||||
return *this;
|
||||
}
|
||||
```
|
||||
|
||||
两个函数的实现都比较中规中矩。 在每个情况中,我们都只从源对象(rhs)中,复制了结构体`Impl`的内容到目标对象中(*this)。我们利用了编译器会为我们自动生成结构体`Impl`的复制操作函数的机制,而不是逐一复制结构体`Impl`的成员,自动生成的复制操作能自动复制每一个成员。 因此我们通过调用`Widget::Impl`的编译器生成的复制操作函数来实现了类`Widget`的复制操作。 在复制构造函数中,注意,我们仍然遵从了Item 21的建议,使用`std::make_unique`而非直接使用`new`。
|
||||
两个函数的实现都比较中规中矩。 在每个情况中,我们都只从源对象(`rhs`)中,复制了结构体`Impl`的内容到目标对象中(`*this`)。我们利用了编译器会为我们自动生成结构体`Impl`的复制操作函数的机制,而不是逐一复制结构体`Impl`的成员,自动生成的复制操作能自动复制每一个成员。 因此我们通过调用编译器生成的`Widget::Impl`的复制操作函数来实现了类`Widget`的复制操作。 在复制构造函数中,注意,我们仍然遵从了[Item21](https://github.com/kelthuzadx/EffectiveModernCppChinese/blob/master/4.SmartPointers/item21.md)的建议,使用`std::make_unique`而非直接使用`new`。
|
||||
|
||||
为了实现`Pimpl`惯用法,`std::unique_ptr`是我们使用的智能指针,因为位于对象内部的`pImpl`指针(例如,在类`Widget`内部),对所指向的对应实现的对象的享有独占所有权(exclusive ownership)。然而,有趣的是,如果我们使用`std::shared_ptr`而不是`std::unique_ptr`来做`pImpl`指针, 我们会发现本节的建议不再适用。 我们不需要在类`Widget`里声明析构函数,也不用用户定义析构函数,编译器将会愉快地生成移动操作,并且将会如我们所期望般工作。代码如下:
|
||||
为了实现Pimpl惯用法,`std::unique_ptr`是我们使用的智能指针,因为位于对象内部的`pImpl`指针(例如,在类`Widget`内部),对所指向的对应实现的对象的享有独占所有权。然而,有趣的是,如果我们使用`std::shared_ptr`而不是`std::unique_ptr`来做`pImpl`指针, 我们会发现本条款的建议不再适用。 我们不需要在类`Widget`里声明析构函数,没有了用户定义析构函数,编译器将会愉快地生成移动操作,并且将会如我们所期望般工作。`widget.h`里的代码如下,
|
||||
|
||||
```cpp
|
||||
//在Widget.h中
|
||||
class Widget{
|
||||
class Widget { //在“widget.h”中
|
||||
public:
|
||||
Widget();
|
||||
... //没有对移动操作和析构函数的声明
|
||||
private:
|
||||
… //没有析构函数和移动操作的声明
|
||||
|
||||
private:
|
||||
struct Impl;
|
||||
std::shared_ptr<Impl> pImpl; //使用std::shared_ptr而非std::unique_ptr
|
||||
}
|
||||
std::shared_ptr<Impl> pImpl; //用std::shared_ptr
|
||||
}; //而不是std::unique_ptr
|
||||
```
|
||||
|
||||
而类`Widget`的使用者,使用`#include widget.h`,可以使用如下代码
|
||||
这是`#include`了`widget.h`的客户代码,
|
||||
|
||||
```cpp
|
||||
Widget w1;
|
||||
auto w2(std::move(w1)); //移动构造w2
|
||||
w1 = std::move(w2); //移动赋值w1
|
||||
auto w2(std::move(w1)); //移动构造w2
|
||||
w1 = std::move(w2); //移动赋值w1
|
||||
```
|
||||
|
||||
这些都能编译,并且工作地如我们所望: `w1`将会被默认构造,它的值会被移动进`w2`,随后值将会被移动回`w1`,然后两者都会被销毁(因此导致指向的`Widget::Impl`对象一并也被销毁)。
|
||||
|
||||
`std::unique_ptr`和`std::shared_ptr`在`pImpl`指针上的表现上的区别的深层原因在于,他们支持自定义销毁器(custom deleter)的方式不同。 对`std::unique_ptr`而言,销毁器的类型是`unique_ptr`的一部分,这让编译器有可能生成更小的运行时数据结构和更快的运行代码。 这种更高效率的后果之一就是`unique_ptr`指向的类型,在编译器的生成特殊成员函数被调用时(如析构函数,移动操作)时,必须已经是一个完成类型。 而对`std::shared_ptr`而言,销毁器的类型不是该智能指针的一部分,这让它会生成更大的运行时数据结构和稍微慢点的代码,但是当编译器生成的特殊成员函数被使用的时候,指向的对象不必是一个完成类型。(译者注: 知道`unique_ptr`和`shared_ptr`的实现,这一段才比较容易理解。)
|
||||
|
||||
对于`pImpl`惯用法而言,在`std::unique_ptr`和`std::shared_ptr`的特性之间,没有一个比较好的折中。 因为对于类`Widget`以及`Widget::Impl`而言,他们是独享占有权关系,这让`std::unique_ptr`使用起来很合适。 然而,有必要知道,在其他情况中,当共享所有权(shared ownership)存在时,`std::shared_ptr`是很适用的选择的时候,没有必要使用`std::unique_ptr`所必需的**声明——定义**(function-definition)这样的麻烦事了。
|
||||
|
||||
**记住**
|
||||
|
||||
- `pImpl`惯用法通过减少在类实现和类使用者之间的编译依赖来减少编译时间。
|
||||
- 对于`std::unique_ptr`类型的`pImpl`指针,需要在头文件的类里声明特殊的成员函数,但是在实现文件里面来实现他们。即使是编译器自动生成的代码可以工作,也要这么做。
|
||||
- 以上的建议只适用于`std::unique_ptr`,不适用于`std::shared_ptr`。
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
这些都能编译,并且工作地如我们所望:`w1`将会被默认构造,它的值会被移动进`w2`,随后值将会被移动回`w1`,然后两者都会被销毁(因此导致指向的`Widget::Impl`对象一并也被销毁)。
|
||||
|
||||
`std::unique_ptr`和`std::shared_ptr`在`pImpl`指针上的表现上的区别的深层原因在于,他们支持自定义删除器的方式不同。 对`std::unique_ptr`而言,删除器的类型是这个智能指针的一部分,这让编译器有可能生成更小的运行时数据结构和更快的运行代码。 这种更高效率的后果之一就是`std::unique_ptr`指向的类型,在编译器的生成特殊成员函数(如析构函数,移动操作)被调用时,必须已经是一个完成类型。 而对`std::shared_ptr`而言,删除器的类型不是该智能指针的一部分,这让它会生成更大的运行时数据结构和稍微慢点的代码,但是当编译器生成的特殊成员函数被使用的时候,指向的对象不必是一个完成类型。(译者注:知道`std::unique_ptr`和`std::shared_ptr`的实现,这一段才比较容易理解。)
|
||||
|
||||
对于Pimpl惯用法而言,在`std::unique_ptr`和`std::shared_ptr`的特性之间,没有一个比较好的折中。 因为对于像`Widget`的类以及像`Widget::Impl`的类之间的关系而言,他们是独享占有权关系,这让`std::unique_ptr`使用起来很合适。 然而,有必要知道,在其他情况中,当共享所有权存在时,`std::shared_ptr`是很适用的选择的时候,就没有`std::unique_ptr`所必需的声明——定义(function-definition)这样的麻烦事了。
|
||||
|
||||
**请记住:**
|
||||
|
||||
- Pimpl惯用法通过减少在类实现和类使用者之间的编译依赖来减少编译时间。
|
||||
- 对于`std::unique_ptr`类型的`pImpl`指针,需要在头文件的类里声明特殊的成员函数,但是在实现文件里面来实现他们。即使是编译器自动生成的代码可以工作,也要这么做。
|
||||
- 以上的建议只适用于`std::unique_ptr`,不适用于`std::shared_ptr`。
|
||||
|
BIN
4.SmartPointers/media/item18_fig1.png
Normal file
BIN
4.SmartPointers/media/item18_fig1.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 10 KiB |
BIN
4.SmartPointers/media/item19_fig1.png
Normal file
BIN
4.SmartPointers/media/item19_fig1.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 25 KiB |
Before Width: | Height: | Size: 11 KiB After Width: | Height: | Size: 11 KiB |
Before Width: | Height: | Size: 17 KiB After Width: | Height: | Size: 17 KiB |
139
Introduction.md
Normal file
139
Introduction.md
Normal file
@ -0,0 +1,139 @@
|
||||
## 简介
|
||||
|
||||
如果你是一个有经验的C++程序员,像我一样,你在初次接触C++11时候会想:“是啊是啊,我明白了。这也是C++,就多了点东西罢了。”但是你接触越多,你会惊讶于改变如此之多。`auto`声明,范围`for`循环,*lambda*表达式,还有右值引用都改变了C++的面貌,不过没有新的并发特性。还有一些地道的表达方式的改变。`0`和`typedef`被请出去了,`nullptr`和别名声明加进来了。枚举现在应该是限域的了。应该更倾向于智能指针而不是原始指针了。移动对象通常比拷贝它们要好了。
|
||||
|
||||
|
||||
有很多C++11的东西要学,先不提C++14了。
|
||||
|
||||
更重要的是,要学习怎样**高效地**使用新机能。如果你需要关于”现代“C++的特性的基础信息,学习资源有很多,但是你想找一些指南,教你怎样应用这些特性来写出正确、高效、可维护、可移植的程序,那就相当有挑战性了。这就是这本书的切入点。它不致力于介绍C++11和C++14的特性,而致力于它们的高效应用。
|
||||
|
||||
书中这些信息被打碎成不同指导方针,称为**条款**。想理解类型推导的不同形式?或者想知道什么时候该用(或者不该用)`auto`声明?你对为什么`const`成员函数应当线程安全,怎样使用`std::unique_ptr`实现Pimpl惯用法,为何要避免*lambda*表达式用默认捕获模式,或者`std::atomic`与`volatile`的区别感兴趣吗?答案都在这里。而且,答案无关于平台,顺应于标准。这本书是关于**可移植**C++的。
|
||||
|
||||
本书的条款是**指导方针**,而不是**规则**,因为指导方针也有例外。每个条款中最关键的部分不是提出的建议,而是建议背后的基本原理。一旦你阅读了它,你就明白你的程序的情况是否违反了条款的指导意见。本书的真正目的不是告诉你应该做什么不应该做什么,而是帮你深入理解C++11和C++14中各种东西是如何工作的。
|
||||
|
||||
### 术语和惯例
|
||||
|
||||
为了保证我们互相理解,对一些术语达成共识非常重要,首先有点讽刺的是,“C++”。有四个C++官方版本,每个版本名字后面带有相应ISO标准被采纳时的年份:C++98,C++03,C++11和C++14。C++98和C++03只有技术细节上的区别,所以本书统称为C++98。当我提到C++11时,我的意思是C++11和C++14,因为C++14是C++11的超集,当我写下C++14,我只意味着C++14。如果我仅仅提到C++,说明适用于所有的语言版本。
|
||||
|
||||
| 我使用的词 | 我意思中的语言版本 |
|
||||
| ---------- | ------------------ |
|
||||
| C++ | 所有版本 |
|
||||
| C++98 | C++98和C++03 |
|
||||
| C++11 | C++11和C++14 |
|
||||
| C++14 | C++14 |
|
||||
|
||||
因此,我可能会说C++重视效率(对所有版本正确),C++98缺少并发的支持(只对C++98和C++03正确),C++11支持*lambda*表达式(对C++11和C++14正确),C++14提供了普遍的函数返回类型推导(只对C++14正确)。
|
||||
|
||||
最遍布C++11各处的特性可能是移动语义了,移动语义的基础是区分右值和左值表达式。那是因为右值表明这个对象适合移动操作,而左值一般不适合。概念上(尽管不经常在实际上用),右值对应于从函数返回的临时对象,而左值对应于你可以引用的(can refer to)对象,或者通过名字,或者通过指针或左值引用。
|
||||
|
||||
对于判断一个表达式是否是左值的一个有用的启发就是,看看能否取得它的地址。如果能取地址,那么通常就是左值。如果不能,则通常是右值。这个启发的好处就是帮你记住,一个表达式的类型与它是左值还是右值无关。也就是说,有个类型`T`,你可以有类型`T`的左值和右值。当你碰到右值引用类型的形参时,记住这一点非常重要,因为形参本身是个左值:
|
||||
|
||||
```cpp
|
||||
class Widget {
|
||||
public:
|
||||
Widget(Widget&& rhs); //rhs是个左值,
|
||||
… //尽管它有个右值引用的类型
|
||||
};
|
||||
```
|
||||
|
||||
在这里,在`Widget`移动构造函数里取`rhs`的地址非常合理,所以`rhs`是左值,尽管它的类型是右值引用。(由于相似的原因,所有形参都是左值。)
|
||||
|
||||
那一小段代码揭示了我通常遵循的惯用法:
|
||||
|
||||
+ 类的名字是`Widget`。每当我想指代任意的用户定义的类型时,我用`Widget`来代表。除非我需要展示类中的特定细节,否则我都直接使用`Widget`而不声明它。
|
||||
|
||||
+ 我使用形参名`rhs`(“right-hand side”)。这是我喜欢的**移动操作**(即移动构造函数和移动赋值运算符)和**拷贝操作**(拷贝构造函数和拷贝构造运算符)的形参名。我也在双目运算符的右侧形参用它:
|
||||
|
||||
```cpp
|
||||
class Widget {
|
||||
public:
|
||||
Widget(Widget&& rhs); //rhs是个左值,
|
||||
… //尽管它有个右值引用的类型
|
||||
};
|
||||
```
|
||||
|
||||
我希望你并不奇怪,我用`lhs`表示“left-hand side”。
|
||||
|
||||
+ 我在部分代码或者部分注释用特殊格式来吸引你的注意。(译者注:但是因为markdown没法在代码块中表明特殊格式,即原书使用的颜色改变和斜体注释,所以大部分情况下只能作罢,少部分地方会有额外说明。)在上面`Widget`移动构造函数中,我高亮了`rhs`的声明和“`rhs`是一个左值”这部分注释。高亮代码不代表写的好坏。只是来提醒你需要额外的注意。
|
||||
|
||||
+ 我使用“`…`”来表示“这里有一些别的代码”。这种窄省略号不同于C++11可变参数模板源代码中的宽省略号(“`...`”)。这听起来不太清楚,但实际并不。比如:
|
||||
|
||||
```cpp
|
||||
template<typename... Ts> //这些是C++源代码的
|
||||
void processVals(const Ts&... params) //省略号
|
||||
{
|
||||
… //这里意思是“这有一些别的代码”
|
||||
}
|
||||
```
|
||||
|
||||
`processVals`的声明表明在声明模板的类型形参时我使用`typename`,但这只是我的个人偏好;关键字`class`可以做同样的事情。在我展示从C++标准中摘录的代码的情况下,我使用`class`声明类型形参,因为那就是标准中的做法。
|
||||
|
||||
当使用另一个同类型的对象来初始化一个对象时,新的对象被称为是来初始化的对象(译者注:initializing object,即源对象)的一个**副本**(*copy*),尽管这个副本是通过移动构造函数创建的。很抱歉地说,C++中没有术语来区别一个对象是拷贝构造的副本还是移动构造的副本(译者注:此处为了区别拷贝这个“动作”与拷贝得到的“东西”,将*copy*按语境译为拷贝(动作)和副本(东西),此处及接下来几段按此方式翻译。在后面的条款中可能会不加区别地全部翻译为“拷贝”。):
|
||||
|
||||
```cpp
|
||||
void someFunc(Widget w); //someFunc的形参w是传值过来
|
||||
|
||||
Widget wid; //wid是个Widget
|
||||
|
||||
someFunc(wid); //在这个someFunc调用中,w是通过拷贝构造函数
|
||||
//创建的副本
|
||||
|
||||
someFunc(std::move(wid)); //在这个someFunc调用中,w是通过移动构造函数
|
||||
//创建的副本
|
||||
```
|
||||
|
||||
右值副本通常由移动构造产生,左值副本通常由拷贝构造产生。如果你仅仅知道一个对象是其他对象的副本,构造这个副本需要花费多大代价是没法说的。比如在上面的代码中,在不知道是用左值还是右值传给`someFunc`情况下,没法说来创建形参`w`花费代价有多大。(你必须还要知道移动和拷贝`Widget`的代价。)
|
||||
|
||||
在函数调用中,调用地传入的表达式称为函数的**实参**(*argument*)。实参被用来初始化函数的**形参**(*parameter*)。在上面第一次调用`someFunc`中,实参为`wid`。在第二次调用中,实参是`std::move(wid)`。两个调用中,形参都是`w`。实参和形参的区别非常重要,因为形参是左值,而用来初始化形参的实参可能是左值或者右值。这一点尤其与**完美转发**(*perfect forwarding*)过程有关,被传给函数的实参以原实参的右值性(*rvalueness*)或左值性(*lvalueness*),再被传给第二个函数。(完美转发讨论细节在[Item30](https://github.com/kelthuzadx/EffectiveModernCppChinese/blob/master/5.RRefMovSemPerfForw/item30.md)。)
|
||||
|
||||
设计优良的函数是**异常安全**(*exception safe*)的,意味着他们至少提供基本的异常安全保证(即基本保证*basic guarantee*)。这样的函数保证调用者在异常抛出时,程序不变量保持完整(即没有数据结构是毁坏的),且没有资源泄漏。有强异常安全保证的函数确保调用者在异常产生时,程序保持在调用前的状态。
|
||||
|
||||
当我提到“**函数对象**”时,我通常指的是某个支持`operator()`成员函数的类型的对象。换句话说,这个对象的行为像函数一样。偶尔我用稍微更普遍一些的术语,表示可以用非成员函数语法调用的任何东西(即“`fuctionName(arguments)`”)。这个广泛定义包括的不仅有支持`operator()`的对象,还有函数和类似C的函数指针。(较窄的定义来自于C++98,广泛点的定义来自于C++11。)将成员函数指针加进来的更深的普遍化产生了我们所知的**可调用对象**(*callable objects*)。你通常可以忽略其中的微小区别,简单地认为函数对象和可调用对象为C++中可以用函数调用语法调用的东西。
|
||||
|
||||
通过*lambda*表达式创建的函数对象称为**闭包**(*closures*)。没什么必要去区别*lambda*表达式和它们创建的闭包,所以我经常把它们统称*lambdas*。类似地,我几乎不区分**函数模板**(*function templates*)(即产生函数的模板)和**模板函数**(*template functions*)(即从函数模板产生的函数)。**类模板**(*class templates*)和**模板类**(*template classes*)同上。
|
||||
|
||||
C++中的许多东西都可被声明和定义。**声明**(*declarations*)引入名字和类型,并不给出比如存放在哪或者怎样实现等的细节:
|
||||
|
||||
```cpp
|
||||
extern int x; //对象声明
|
||||
|
||||
class Widget; //类声明
|
||||
|
||||
bool func(const Widget& w); //函数声明
|
||||
|
||||
enum class Color; //限域enum声明(见条款10)
|
||||
```
|
||||
|
||||
**定义**(*definitions*)提供存储位置或者实现细节:
|
||||
|
||||
```cpp
|
||||
int x; //对象定义
|
||||
|
||||
class Widget { //类定义
|
||||
…
|
||||
};
|
||||
|
||||
bool func(const Widget& w)
|
||||
{ return w.size() < 10; } //函数定义
|
||||
|
||||
enum class Color
|
||||
{ Yellow, Red, Blue }; //限域enum定义
|
||||
```
|
||||
|
||||
定义也有资格称为声明,所以我倾向于只有声明,除非这个东西有个定义非常重要。
|
||||
|
||||
我定义一个函数的**签名**(*signature*)为它声明的一部分,这个声明指定了形参类型和返回类型。函数名和形参名不是签名的一部分。在上面的例子中,`func`的签名是`bool(const Widget&)`。函数声明中除了形参类型和返回类型之外的元素(比如`noexcept`或者`constexpr`,如果存在的话)都被排除在外。(`noexcept`和`constexpr`在[Item14](https://github.com/kelthuzadx/EffectiveModernCppChinese/blob/master/3.MovingToModernCpp/item14.md)和[15](https://github.com/kelthuzadx/EffectiveModernCppChinese/blob/master/3.MovingToModernCpp/item15.md)叙述。)“签名”的官方定义和我的有点不一样,但是对本书来捉,我的定义更有用。(官方定义有时排除返回类型。)
|
||||
|
||||
新的C++标准保持了旧标准写的代码的有效性,但是偶尔标准化委员会**废弃**(*deprecate*)一些特性。这些特性在标准化的“死囚区”中,可能在未来的标准中被移除。编译器可能警告也可能不警告这些废弃特性的使用,但是你应当尽量避免使用它们。它们不仅可能导致将来对移植的头痛,也通常不如来替代它们的新特性。例如,`std::auto_ptr`在C++11中被废弃,因为`std::unique_ptr`可以做同样的工作,而且只会做的更好。
|
||||
|
||||
有时标准说一个操作的结果有**未定义的表现**(*undefined behavior*)。这意味着运行时表现是不可预测的,不用说你也想避开这种不确定性。有未定义表现的行动的例子是,在`std::vector`范围外使用方括号(“`[]`”),解引用未初始化的迭代器,或者引入数据竞争(即有两个或以上线程,至少一个是writer,同时访问相同的内存位置)。
|
||||
|
||||
我将那些比如从`new`返回的内置指针(*build-in pointers*)称为**原始指针**(*raw pointers*)。原始指针的“反义词”是**智能指针**(*smart pointers*)。智能指针通常重载指针解引用运算符(`operator->`和`operator*`),但在[Item20](https://github.com/kelthuzadx/EffectiveModernCppChinese/blob/master/4.SmartPointers/item20.md)中解释看`std::weak_ptr`是个例外。
|
||||
|
||||
在源代码注释中,我有时将“constructor”(构造函数)缩写为`ctor`,将“destructor”(析构函数)缩写为`dtor`。(译者注:但译文中基本都完整翻译了而没使用缩写。)
|
||||
|
||||
### 报告bug,提出改进意见
|
||||
|
||||
我尽力将本书写的清晰、准确、富含有用的信息,但是当然还有些去做得更好的办法。如果你找到了任何类型的错误(技术上的,叙述上的,语法上的,印刷上的等),或者有些建议如何改进本书,请给我发电子邮件到emc++@aristeia.com。新的印刷给了我改进《Modern Effective C++》的机会,但我也不能解决我不知道的问题!
|
||||
|
||||
要查看我所知道的事情,参见本书勘误表页,http://www.aristeia.com/BookErrata/emc++-errata.html 。
|
@ -36,7 +36,7 @@
|
||||
4. __智能指针__
|
||||
1. [Item 18:对于独占资源使用std::unique_ptr](https://github.com/kelthuzadx/EffectiveModernCppChinese/blob/master/4.SmartPointers/item18.md) 由 @wendajiang贡献
|
||||
2. [Item 19:对于共享资源使用std::shared_ptr](https://github.com/kelthuzadx/EffectiveModernCppChinese/blob/master/4.SmartPointers/item19.md) 已修订
|
||||
3. [Item 20:像std::shared_ptr一样使用std::weak_ptr可能造成dangle](https://github.com/kelthuzadx/EffectiveModernCppChinese/blob/master/4.SmartPointers/item20.md) 更新完成
|
||||
3. [Item 20:当std::shard_ptr可能悬空时使用std::weak_ptr](https://github.com/kelthuzadx/EffectiveModernCppChinese/blob/master/4.SmartPointers/item20.md) 更新完成
|
||||
4. [Item 21:优先考虑使用std::make_unique和std::make_shared而非new](https://github.com/kelthuzadx/EffectiveModernCppChinese/blob/master/4.SmartPointers/item21.md) 由 @pusidun贡献
|
||||
5. [Item 22:当使用Pimpl惯用法,请在实现文件中定义特殊成员函数](https://github.com/kelthuzadx/EffectiveModernCppChinese/blob/master/4.SmartPointers/item22.md) 由 @BlurryLight贡献
|
||||
5. __右值引用,移动语意,完美转发__
|
||||
|
Loading…
Reference in New Issue
Block a user