mirror of
https://github.com/CnTransGroup/EffectiveModernCppChinese.git
synced 2024-12-25 20:30:21 +08:00
parent
c4295e5ab4
commit
7559edafc3
@ -36,9 +36,9 @@ private:
|
||||
|
||||
因为类`Widget`不再提到类型`std::string`,`std::vector`以及`Gadget`,`Widget`的使用者不再需要为了这些类型而引入头文件。 这可以加速编译,并且意味着,如果这些头文件中有所变动,`Widget`的使用者不会受到影响。
|
||||
|
||||
一个已经被声明,却还未被实现的类型,被称为**未完成类型**(*incomplete type*)。 `Widget::Impl`就是这种类型。 你能对一个未完成类型做的事很少,但是声明一个指向它的指针是可以的。Pimpl惯用法利用了这一点。
|
||||
一个已经被声明,却还未被实现的类型,被称为**不完整类型**(*incomplete type*)。 `Widget::Impl`就是这种类型。 你能对一个不完整类型做的事很少,但是声明一个指向它的指针是可以的。Pimpl惯用法利用了这一点。
|
||||
|
||||
Pimpl惯用法的第一步,是声明一个数据成员,它是个指针,指向一个未完成类型。 第二步是动态分配和回收一个对象,该对象包含那些以前在原来的类中的数据成员。 内存分配和回收的代码都写在实现文件里,比如,对于类`Widget`而言,写在`Widget.cpp`里:
|
||||
Pimpl惯用法的第一步,是声明一个数据成员,它是个指针,指向一个不完整类型。 第二步是动态分配和回收一个对象,该对象包含那些以前在原来的类中的数据成员。 内存分配和回收的代码都写在实现文件里,比如,对于类`Widget`而言,写在`Widget.cpp`里:
|
||||
|
||||
```cpp
|
||||
#include "widget.h" //以下代码均在实现文件“widget.cpp”里
|
||||
@ -106,13 +106,13 @@ Widget::Widget() //根据条款21,通过std::make_unique
|
||||
Widget w; //错误!
|
||||
```
|
||||
|
||||
你所看到的错误信息根据编译器不同会有所不同,但是其文本一般会提到一些有关于“把`sizeof`或`delete`应用到未完成类型上”的信息。对于未完成类型,使用以上操作是禁止的。
|
||||
你所看到的错误信息根据编译器不同会有所不同,但是其文本一般会提到一些有关于“把`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`被析构时(例如离开了作用域),问题出现了。在这个时候,它的析构函数被调用。我们在类的定义里使用了`std::unique_ptr`,所以我们没有声明一个析构函数,因为我们并没有任何代码需要写在里面。根据编译器自动生成的特殊成员函数的规则(见 [Item17](../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`被创建的那行,因为这行代码明确地构造了这个对象,导致了后面潜在的析构。
|
||||
在对象`w`被析构时(例如离开了作用域),问题出现了。在这个时候,它的析构函数被调用。我们在类的定义里使用了`std::unique_ptr`,所以我们没有声明一个析构函数,因为我们并没有任何代码需要写在里面。根据编译器自动生成的特殊成员函数的规则(见 [Item17](../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::Impl>`的代码之前, `Widget::Impl`已经是一个完成类型(*complete type*)。 当编译器“看到”它的定义的时候,该类型就成为完成类型了。 但是 `Widget::Impl`的定义在`widget.cpp`里。成功编译的关键,就是在`widget.cpp`文件内,让编译器在“看到” `Widget`的析构函数实现之前(也即编译器插入的,用来销毁`std::unique_ptr`这个数据成员的代码的,那个位置),先定义`Widget::Impl`。
|
||||
为了解决这个问题,你只需要确保在编译器生成销毁`std::unique_ptr<Widget::Impl>`的代码之前, `Widget::Impl`已经是一个完整类型(*complete type*)。 当编译器“看到”它的定义的时候,该类型就成为完整类型了。 但是 `Widget::Impl`的定义在`widget.cpp`里。成功编译的关键,就是在`widget.cpp`文件内,让编译器在“看到” `Widget`的析构函数实现之前(也即编译器插入的,用来销毁`std::unique_ptr`这个数据成员的代码的,那个位置),先定义`Widget::Impl`。
|
||||
|
||||
做出这样的调整很容易。只需要先在`widget.h`里,只声明类`Widget`的析构函数,但不要在这里定义它:
|
||||
|
||||
@ -175,7 +175,7 @@ private: //跟之前一样
|
||||
};
|
||||
```
|
||||
|
||||
这样的做法会导致同样的错误,和之前的声明一个不带析构函数的类的错误一样,并且是因为同样的原因。 编译器生成的移动赋值操作符,在重新赋值之前,需要先销毁指针`pImpl`指向的对象。然而在`Widget`的头文件里,`pImpl`指针指向的是一个未完成类型。移动构造函数的情况有所不同。 移动构造函数的问题是编译器自动生成的代码里,包含有抛出异常的事件,在这个事件里会生成销毁`pImpl`的代码。然而,销毁`pImpl`需要`Impl`是一个完成类型。
|
||||
这样的做法会导致同样的错误,和之前的声明一个不带析构函数的类的错误一样,并且是因为同样的原因。 编译器生成的移动赋值操作符,在重新赋值之前,需要先销毁指针`pImpl`指向的对象。然而在`Widget`的头文件里,`pImpl`指针指向的是一个不完整类型。移动构造函数的情况有所不同。 移动构造函数的问题是编译器自动生成的代码里,包含有抛出异常的事件,在这个事件里会生成销毁`pImpl`的代码。然而,销毁`pImpl`需要`Impl`是一个完整类型。
|
||||
|
||||
因为这个问题同上面一致,所以解决方案也一样——把移动操作的定义移动到实现文件里:
|
||||
```cpp
|
||||
@ -273,7 +273,7 @@ w1 = std::move(w2); //移动赋值w1
|
||||
|
||||
这些都能编译,并且工作地如我们所望:`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`的实现,这一段才比较容易理解。)
|
||||
`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)这样的麻烦事了。
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user