diff --git a/4.SmartPointers/item22.html b/4.SmartPointers/item22.html index 2964bb7..0f555b4 100644 --- a/4.SmartPointers/item22.html +++ b/4.SmartPointers/item22.html @@ -167,8 +167,8 @@ private: };

因为类Widget不再提到类型std::stringstd::vector以及GadgetWidget的使用者不再需要为了这些类型而引入头文件。 这可以加速编译,并且意味着,如果这些头文件中有所变动,Widget的使用者不会受到影响。

-

一个已经被声明,却还未被实现的类型,被称为未完成类型incomplete type)。 Widget::Impl就是这种类型。 你能对一个未完成类型做的事很少,但是声明一个指向它的指针是可以的。Pimpl惯用法利用了这一点。

-

Pimpl惯用法的第一步,是声明一个数据成员,它是个指针,指向一个未完成类型。 第二步是动态分配和回收一个对象,该对象包含那些以前在原来的类中的数据成员。 内存分配和回收的代码都写在实现文件里,比如,对于类Widget而言,写在Widget.cpp里:

+

一个已经被声明,却还未被实现的类型,被称为不完整类型incomplete type)。 Widget::Impl就是这种类型。 你能对一个不完整类型做的事很少,但是声明一个指向它的指针是可以的。Pimpl惯用法利用了这一点。

+

Pimpl惯用法的第一步,是声明一个数据成员,它是个指针,指向一个不完整类型。 第二步是动态分配和回收一个对象,该对象包含那些以前在原来的类中的数据成员。 内存分配和回收的代码都写在实现文件里,比如,对于类Widget而言,写在Widget.cpp里:

#include "widget.h"             //以下代码均在实现文件“widget.cpp”里
 #include "gadget.h"
 #include <string>
@@ -221,10 +221,10 @@ Widget::Widget()                    //根据条款21,通过std::make_unique
 
 Widget w;                           //错误!
 
-

你所看到的错误信息根据编译器不同会有所不同,但是其文本一般会提到一些有关于“把sizeofdelete应用到未完成类型上”的信息。对于未完成类型,使用以上操作是禁止的。

-

在Pimpl惯用法中使用std::unique_ptr会抛出错误,有点惊悚,因为第一std::unique_ptr宣称它支持未完成类型,第二Pimpl惯用法是std::unique_ptr的最常见的使用情况之一。 幸运的是,让这段代码能正常运行很简单。 只需要对上面出现的问题的原因有一个基础的认识就可以了。

-

在对象w被析构时(例如离开了作用域),问题出现了。在这个时候,它的析构函数被调用。我们在类的定义里使用了std::unique_ptr,所以我们没有声明一个析构函数,因为我们并没有任何代码需要写在里面。根据编译器自动生成的特殊成员函数的规则(见 Item17),编译器会自动为我们生成一个析构函数。 在这个析构函数里,编译器会插入一些代码来调用类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

+

你所看到的错误信息根据编译器不同会有所不同,但是其文本一般会提到一些有关于“把sizeofdelete应用到不完整类型上”的信息。对于不完整类型,使用以上操作是禁止的。

+

在Pimpl惯用法中使用std::unique_ptr会抛出错误,有点惊悚,因为第一std::unique_ptr宣称它支持不完整类型,第二Pimpl惯用法是std::unique_ptr的最常见的使用情况之一。 幸运的是,让这段代码能正常运行很简单。 只需要对上面出现的问题的原因有一个基础的认识就可以了。

+

在对象w被析构时(例如离开了作用域),问题出现了。在这个时候,它的析构函数被调用。我们在类的定义里使用了std::unique_ptr,所以我们没有声明一个析构函数,因为我们并没有任何代码需要写在里面。根据编译器自动生成的特殊成员函数的规则(见 Item17),编译器会自动为我们生成一个析构函数。 在这个析构函数里,编译器会插入一些代码来调用类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

做出这样的调整很容易。只需要先在widget.h里,只声明类Widget的析构函数,但不要在这里定义它:

class Widget {                  //跟之前一样,在“widget.h”中
 public:
@@ -274,7 +274,7 @@ private:                                        //跟之前一样
     std::unique_ptr<Impl> pImpl;
 };
 
-

这样的做法会导致同样的错误,和之前的声明一个不带析构函数的类的错误一样,并且是因为同样的原因。 编译器生成的移动赋值操作符,在重新赋值之前,需要先销毁指针pImpl指向的对象。然而在Widget的头文件里,pImpl指针指向的是一个未完成类型。移动构造函数的情况有所不同。 移动构造函数的问题是编译器自动生成的代码里,包含有抛出异常的事件,在这个事件里会生成销毁pImpl的代码。然而,销毁pImpl需要Impl是一个完成类型。

+

这样的做法会导致同样的错误,和之前的声明一个不带析构函数的类的错误一样,并且是因为同样的原因。 编译器生成的移动赋值操作符,在重新赋值之前,需要先销毁指针pImpl指向的对象。然而在Widget的头文件里,pImpl指针指向的是一个不完整类型。移动构造函数的情况有所不同。 移动构造函数的问题是编译器自动生成的代码里,包含有抛出异常的事件,在这个事件里会生成销毁pImpl的代码。然而,销毁pImpl需要Impl是一个完整类型。

因为这个问题同上面一致,所以解决方案也一样——把移动操作的定义移动到实现文件里:

class Widget {                          //仍然在“widget.h”中
 public:
@@ -353,7 +353,7 @@ auto w2(std::move(w1));     //移动构造w2
 w1 = std::move(w2);         //移动赋值w1
 

这些都能编译,并且工作地如我们所望:w1将会被默认构造,它的值会被移动进w2,随后值将会被移动回w1,然后两者都会被销毁(因此导致指向的Widget::Impl对象一并也被销毁)。

-

std::unique_ptrstd::shared_ptrpImpl指针上的表现上的区别的深层原因在于,他们支持自定义删除器的方式不同。 对std::unique_ptr而言,删除器的类型是这个智能指针的一部分,这让编译器有可能生成更小的运行时数据结构和更快的运行代码。 这种更高效率的后果之一就是std::unique_ptr指向的类型,在编译器的生成特殊成员函数(如析构函数,移动操作)被调用时,必须已经是一个完成类型。 而对std::shared_ptr而言,删除器的类型不是该智能指针的一部分,这让它会生成更大的运行时数据结构和稍微慢点的代码,但是当编译器生成的特殊成员函数被使用的时候,指向的对象不必是一个完成类型。(译者注:知道std::unique_ptrstd::shared_ptr的实现,这一段才比较容易理解。)

+

std::unique_ptrstd::shared_ptrpImpl指针上的表现上的区别的深层原因在于,他们支持自定义删除器的方式不同。 对std::unique_ptr而言,删除器的类型是这个智能指针的一部分,这让编译器有可能生成更小的运行时数据结构和更快的运行代码。 这种更高效率的后果之一就是std::unique_ptr指向的类型,在编译器的生成特殊成员函数(如析构函数,移动操作)被调用时,必须已经是一个完整类型。 而对std::shared_ptr而言,删除器的类型不是该智能指针的一部分,这让它会生成更大的运行时数据结构和稍微慢点的代码,但是当编译器生成的特殊成员函数被使用的时候,指向的对象不必是一个完整类型。(译者注:知道std::unique_ptrstd::shared_ptr的实现,这一段才比较容易理解。)

对于Pimpl惯用法而言,在std::unique_ptrstd::shared_ptr的特性之间,没有一个比较好的折中。 因为对于像Widget的类以及像Widget::Impl的类之间的关系而言,他们是独享占有权关系,这让std::unique_ptr使用起来很合适。 然而,有必要知道,在其他情况中,当共享所有权存在时,std::shared_ptr是很适用的选择的时候,就没有std::unique_ptr所必需的声明——定义(function-definition)这样的麻烦事了。

请记住: