Update item22.md (#162)

修改`incomplete type`翻译,改为符合广泛用词的`不完整类型`
This commit is contained in:
L-Super 2023-08-23 22:32:55 +08:00 committed by GitHub
parent c4295e5ab4
commit 7559edafc3
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23

View File

@ -36,9 +36,9 @@ private:
因为类`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惯用法的第一步是声明一个数据成员它是个指针指向一个未完成类型。 第二步是动态分配和回收一个对象,该对象包含那些以前在原来的类中的数据成员。 内存分配和回收的代码都写在实现文件里,比如,对于类`Widget`而言,写在`Widget.cpp`里: Pimpl惯用法的第一步是声明一个数据成员它是个指针指向一个不完整类型。 第二步是动态分配和回收一个对象,该对象包含那些以前在原来的类中的数据成员。 内存分配和回收的代码都写在实现文件里,比如,对于类`Widget`而言,写在`Widget.cpp`里:
```cpp ```cpp
#include "widget.h" //以下代码均在实现文件“widget.cpp”里 #include "widget.h" //以下代码均在实现文件“widget.cpp”里
@ -106,13 +106,13 @@ Widget::Widget() //根据条款21通过std::make_unique
Widget w; //错误! 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`的析构函数,但不要在这里定义它: 做出这样的调整很容易。只需要先在`widget.h`里,只声明类`Widget`的析构函数,但不要在这里定义它:
@ -175,7 +175,7 @@ private: //跟之前一样
}; };
``` ```
这样的做法会导致同样的错误,和之前的声明一个不带析构函数的类的错误一样,并且是因为同样的原因。 编译器生成的移动赋值操作符,在重新赋值之前,需要先销毁指针`pImpl`指向的对象。然而在`Widget`的头文件里,`pImpl`指针指向的是一个未完成类型。移动构造函数的情况有所不同。 移动构造函数的问题是编译器自动生成的代码里,包含有抛出异常的事件,在这个事件里会生成销毁`pImpl`的代码。然而,销毁`pImpl`需要`Impl`是一个完类型。 这样的做法会导致同样的错误,和之前的声明一个不带析构函数的类的错误一样,并且是因为同样的原因。 编译器生成的移动赋值操作符,在重新赋值之前,需要先销毁指针`pImpl`指向的对象。然而在`Widget`的头文件里,`pImpl`指针指向的是一个不完整类型。移动构造函数的情况有所不同。 移动构造函数的问题是编译器自动生成的代码里,包含有抛出异常的事件,在这个事件里会生成销毁`pImpl`的代码。然而,销毁`pImpl`需要`Impl`是一个完类型。
因为这个问题同上面一致,所以解决方案也一样——把移动操作的定义移动到实现文件里: 因为这个问题同上面一致,所以解决方案也一样——把移动操作的定义移动到实现文件里:
```cpp ```cpp
@ -273,7 +273,7 @@ w1 = std::move(w2); //移动赋值w1
这些都能编译,并且工作地如我们所望:`w1`将会被默认构造,它的值会被移动进`w2`,随后值将会被移动回`w1`,然后两者都会被销毁(因此导致指向的`Widget::Impl`对象一并也被销毁)。 这些都能编译,并且工作地如我们所望:`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这样的麻烦事了。 对于Pimpl惯用法而言在`std::unique_ptr`和`std::shared_ptr`的特性之间,没有一个比较好的折中。 因为对于像`Widget`的类以及像`Widget::Impl`的类之间的关系而言,他们是独享占有权关系,这让`std::unique_ptr`使用起来很合适。 然而,有必要知道,在其他情况中,当共享所有权存在时,`std::shared_ptr`是很适用的选择的时候,就没有`std::unique_ptr`所必需的声明——定义function-definition这样的麻烦事了。