本文翻译自《effective modern C++》,由于水平有限,故无法保证翻译完全正确,欢迎指出错误。谢谢!

博客已经迁移到这里啦

如果你曾经同过久的编译时间斗争过,那么你肯定对Pimpl("point to implementation",指向实现)机制很熟悉了。这种技术让你把类的数据成员替换成指向一个实现类(或结构)的指针,把曾经放在主类中的数据成员放到实现类中去,然后通过指针间接地访问那些数据成员。举个例子,假设Widget看起来像这个样子:

class Widget{                   // 在头文件"widget.h"中
public:Widget();...
private:std::string name;std::vector<double> data;   Gadget g1, g2, g3;          // Gadget是用户自定义的类型
};

因为Widget的数据成员包含std::string,std::vector和Gadget类型,这些类型的头文件必须出现在Widget的编译中,这就意味着Widget的客户必须#include <string>,<vector>,和gadget.h。这些头文件增加了Widget客户的编译时间,加上它们使得这些客户依赖于头文件的内容。如果头文件的内容改变了,Widget的客户必须重编译。标准头文件<string><vector>不会经常改变,但是gadget.h有频繁更替版本的倾向。

在C++98中应用Pimpl机制需要在Widget中把它的数据成员替换成一个原始指针,指向一个已经被声明却还没有定义的结构:

class Widget{                       // 还是在头文件"widget.h"中
public:Widget();~Widget();                      // 看下面的内容可以得知析构函数是需要的...private:struct Impl;                    // 声明一个要实现的结构Impl *pImpl;                    // 并用指针指向它
};

因为Widget不在涉及类型std::string, std::vector和Gadget,所以Widget的客户不再需要#include这些类型的头文件了。这加快了编译速度,并且这也意味着如果头文件有了一些变化,Widget的客户是不受影响的。

一个被声明却还没有定义的类型被称为一个不完整类型(incomplete type)。Widget::Impl就是这样的类型。对于一个不完整类型,你能做的事情很少,但是定义一个指针指向它们是可以的。Pimpl机制就是利用了这一点。

Pimpl机制的第一步就是声明一个数据成员指向一个不完整类型。第二步是动态分配和归还这个类型的对象,这个对象持有曾经在源类(没使用Pimpl机制时的类)中的数据成员。分配和归还代码写在实现文件中,比如,对于Widget来说,就在widget.cpp中:

#include "widget.h"             //在实现文件"widget.cpp"中
#include "gadget.h"
#include <string>
#include <vector>struct Widget::Impl{            // 带有之前在Widget中的数据成员的std::string name;           // Widget::Impl的定义std::vector<double> data;Gadget g1, g2, g3;
};Widget::Widget()                // 分配Widget对象的数据成员
: pImpl(new Impl)
{}Widget::~Widget()               // 归还这个对象的数据成员
{ delete pImpl; }

这里我显示的#include指令表明了,总的来说,对std::string, std::vector, 和Gadget的头文件的依赖性还是存在的,但是,这些依赖性已经从widget.h(这是对Widget客户可见以及被他使用的)转移到了widget.cpp(这是只对Widget的实现者可见以及只被实现者所使用的)。我已经高亮了代码中动态分配和归还Impl对象的地方(译注:就是new Impl和 delete pImpl)。为了当Widget销毁的时候归还这个对象,我们就需要使用Widget的析构函数。

但是我显示给你的是C++98的代码,并且这散发着浓浓的旧时代的气息。它使用原始指针和原始的new,delete,怎么说呢,就是太原始了。这一章的主题是智能指针优于原始指针,所以如果我们想在Widget构造函数中动态分配一个Widget::Impl对象,并且让它的销毁时间和Widget一样,std::unique_ptr(看Item 18)这个工具完全符合我们的需要。把原始pImpl指针替换成std::unique_ptr在头文件中产生出这样的代码:

class Widget{
public:Widget();...private:struct Impl;                            // 使用智能指针来替换原始指针std::unique_ptr<Impl> pImpl;
};

然后在实现文件中是这样的:

#include "widget.h"
#include "gadget.h"
#include <string>
#include <vector>struct Widget::Impl {                       // 和以前一样std::string name;std::vector<double> data;Gadget g1, g2, g3;
};Widget::Widget()                            // 通过std::make_unique
: pImpl(std::make_unique<Impl>())           // 来创建一个std::unique_ptr
{}                                          

你应该已经注意到Widget的析构函数不存在了。这是因为我们没有任何代码要放到它里面。当std::unique_ptr销毁时,它自动销毁它指向的对象,所以我们自己没必要再delete任何东西。这是智能指针吸引人的一个地方:它们消除了手动释放资源的需求。

这段代码能编译通过,但是,可悲的是,客户无法使用:

#include "widget.h"Widget w;                   // 错误

你收到的错误信息取决于你使用的编译器,但是它通常涉及到把sizeof或delete用到一个不完整类型上。这些操作都不是你使用这种类型(不完整类型)能做的操作。

使用std::unique_ptr造成的这种表面上的错误是很令人困扰的,因为(1)std::unique_ptr声称自己是支持不完整类型的,并且(2)Pimpl机制是std::unique_ptr最常见的用法。幸运的是,让代码工作起来是很容易的。所有需要做的事就是理解什么东西造成了这个问题。

问题发生在w销毁的时候产生的代码(比如,离开了作用域)。在这个时候,它的析构函数被调用。在类定义中使用std::unique_ptr,我们没有声明一个析构函数,因为我们不需要放任何代码进去。同通常的规则(看Item 17)相符合,编译器为我们产生出析构函数。在析构函数中,编译器插入代码调用Widget的数据成员pImpl的析构函数。pImpl是一个std::unique_ptr<:impl>,也就是一个使用了默认deleter的std::unique_ptr。默认deleter是一个函数,这个函数在std::unqieu_ptr中把delete用在原始指针上,但是,实现中,常常让默认deleter调用C++11的static_assert来确保原始指针没有指向一个不完整类型。然后,当编译器为Widget w产生析构函数的代码时,它就碰到一个失败的static_assert,这也就是导致错误消息的原因了。这个错误消息应该指向w销毁的地方,但是因为Widget的析构函数和所有的“编译器产生的”特殊成员函数一样,是隐式内联的。所以错误消息常常指向w创建的那一行,因为它的源代码显式创建的对象之后会导致隐式的销毁调用。

调整起来很简单,在widget.h中声明Widget的的析构函数,但是不在这定义它:

class Widget {
public:Widget();~Widget();                          // 只声明...private:struct Impl;std::unique_ptr<Impl> pImpl;
};

然后在widget.cpp中于Widget::Impl之后进行定义:

#include "widget.h"
#include "gadget.h"
#include <string>
#include <vector>struct Widget::Impl { std::string name; std::vector<double> data;Gadget g1, g2, g3;
};Widget::Widget()
: pImpl(std::make_unique<Impl>())
{}Widget::~Widget()                       // ~Widget的定义
{}

这工作得很好,并且它要码的字最少,但是如果你想要强调“编译器产生的”析构函数可以做到正确的事情(也就是你声明它的唯一原因就是让它的定义在Widget的实现文件中产生),那么你就能在定义析构函数的时候使用“=default”:

Widget::~Widget() = default;            //和之前的效果是一样的

使用Pimpl机制的类是可以支持move操作的,因为“编译器产生的”move操作是我们需要的:执行一个move操作在std::unique_ptr上。就像Item 17解释的那样,在Widget中声明一个析构函数会阻止编译器产生move操作,所以如果你想支持move操作,你必须自己声明这些函数。如果“编译器产生的”版本是正确的行为,你可能会尝试像下面这样实现:

class Widget {
public:Widget();~Widget();Widget(Widget&& rhs) = default;                 // 想法是对的Widget& operator=(Widget&& rhs) = default;      // 代码却是错的                           ...private:struct Impl;std::unique_ptr<Impl> pImpl;
};

这个方法将导致和不声明析构函数同样的问题,并且是出于同样的根本性的原因。“编译器产生的”operator move在重新赋值前,需要销毁被pImpl指向的对象,但是在Widget的头文件中,pImpl指向一个不完整类型。move构造函数的情况和赋值函数是不同的。构造函数的问题是,万一一个异常在move构造函数中产生,编译器通常要产生出代码来销毁pImpl,然后销毁pImpl需要Impl是完整的。

因为问题和之前一样,所以修复方法也一样:把move操作的定义移动到实现文件中去:

class Widget {
public:Widget();~Widget();Widget(Widget&& rhs);                   // 只定义Widget& operator=(Widget&& rhs);        // 不实现...private:struct Impl;std::unique_ptr<Impl> pImpl;
};#include <string>
…                                           // 在"widget.cpp"中struct Widget::Impl { … };                  // 和之前一样Widget::Widget()
: pImpl(std::make_unique<Impl>())
{}Widget::~Widget() = default; Widget::Widget(Widget&& rhs) = default;             // 定义
Widget& Widget::operator=(Widget&& rhs) = default;  // 定义

Pimpl机制是减少类的实现和类的客户之间的编译依赖性的方法,但是从概念上来说,使用这个机制不会改变类所代表的东西。源Widget类包含std::string,std::vector和Gadet数据成员,并且,假设Gadget和std::string以及std::vector一样,是能拷贝的,那么让Widget支持拷贝操作是有意义的。我们必须自己写这些函数,因为(1)编译器不会为“只能移动的类型”(比如std::unique_ptr)产生出拷贝操作,(2)即使他们会这么做,产生的函数也只会拷贝std::unique_ptr(也就是执行浅拷贝),但是我们想要拷贝指针指向的东西(也就是执行深拷贝)。

按照我们已经熟悉的惯例,我们在头文件中声明函数,并且在实现文件中实现它:

class Widget {                              // 在"widget.h"中
public:…                                       // 和之前一样的其他函数Widget(const Widget& rhs);              // 声明Widget& operator=(const Widget& rhs);   // 声明private: struct Impl;std::unique_ptr<Impl> pImpl;
};#include "widget.h"
…                                           // 在"widget.cpp"中struct Widget::Impl { … }; Widget::~Widget() = default; Widget::Widget(const Widget& rhs)               // 拷贝构造函数
: pImpl(std::make_unique<Impl>(*rhs.pImpl))
{}Widget& Widget::operator=(const Widget& rhs)    // 拷贝operator=
{*pImpl = *rhs.pImpl;return *this;
}

两个函数的实现都很方便。每种情况,我们都只是简单地从源对象(rhs)中把Impl结构拷贝到目标对象(*this)。比起一个个地拷贝成员,我们利用了一个事实,也就是编译器会为Impl创造出拷贝操作,然后这些操作会自动地拷贝每一个成员。因此我们是通过调用Widget::Impl的“编译器产生的”拷贝操作来实现Widget的拷贝操作的,记住,我们还是要遵循Item 21的建议,比起直接使用new,优先使用std::make_unique。

为了实现Pimpl机制,std::unique_ptr是被使用的智能指针,因为对象(也就是Widget)内部的pImpl指针对相应的实现对象(比如,Widget::Impl对象)有独占所有权的语义。这很有趣,所以记住,如果我们使用std::shared_ptr来代替std::unique_ptr用在pImpl身上,我们将发现对于本Item的建议不再使用了。我们不需要声明Widget的析构函数,并且如果没有自定义的析构函数,编译器将很高兴地为我们产生出move操作,这些都是我们想要的。给出widget.h中的代码,

class Widget{                       //在"widget.h"中
public:Widget();                   ...                             //不需要声明析构函数和move操作private:struct Impl;                    std::shared_ptr<Impl> pImpl;    //用std::shared_ptr代替
};                                  //std::unique_ptr

然后#include widget.h的客户代码,

Widget w1;auto w2(std::move(w1));         //move构造w2w1 = std::move(w2);             //move赋值w1

所有的东西都能编译并执行得和我们希望的一样:w1将被默认构造,它的值将移动到w2中去,这个值之后将移动回w1,并且最后w1和w2都将销毁(因此造成指向的Widget::Impl对象被销毁)。

std::unique_ptr和std::shared_ptr对于pImpl指针行为的不同源于这两个智能指针对于自定义deleter的不同的支持方式。对于std::unique_ptr来说,deleter的类型是智能指针类型的一部分,并且这让编译器产生出更小的运行期数据结构和更快的运行期代码成为可能。这样的高效带来的结果就是,当“编译器产生的”特殊函数(也就是,析构函数和move操作)被使用的时候,指向的类型必须是完整的。对于std::shared_ptr,deleter的类型不是智能指针的一部分。这就需要更大的运行期数据结构和更慢的代码,但是当“编译器产生的”特殊函数被使用时,指向的类型不需要是完整的。

对于Pimpl机制来说,std::unique_ptr和std::shared_ptr之间没有明确的抉择,因为Widget和Widget::Impl之间的关系是独占所有权的关系,所以这使得std::unique_ptr成为更合适的工具。但是,值得我们注意的是另外一种情况,这种情况下共享所有权是存在的(因此std::shared_ptr是更合适的设计选择),我们就不需要做那么多的函数定义了(如果使用std::unique_ptr的话是要做的)。

            你要记住的事
  • Pimpl机制通过降低类客户和类实现之间的编译依赖性来降低编译时间。
  • 对于std::unique_ptr的pImpl指针,在头文件中声明特殊成员函数,但是实现他们的时候要放在实现文件中实现。即使编译器提供的默认函数实现是满足设计需要,我们还是要这么做。
  • 上面的建议能用在std::unique_ptr上面,但是不能用在std::shared_ptr上面。

转载于:https://www.cnblogs.com/boydfd/p/5161128.html

Item 22: 当使用Pimpl机制时,在实现文件中给出特殊成员函数的实现相关推荐

  1. function函数嵌套 matlab_matlab – 当没有使用“end”时,一个.m文件中的多个函数是嵌套的还是本地的...

    在MATLAB中,您可以在一个.m文件中拥有多个函数.当然有主要功能,然后是 nested or local functions. 每种功能类型的示例: % myfunc.m with local f ...

  2. 使用fastapi时在py文件中无法正常引用

    问题描述:在使用fastapi快速构建一个web应用时,已经使用poetry add fastapi下载了fastapi的依赖包,但是在py文件中通过下述语法去引入fastapi时,被告知无法引入该包 ...

  3. 《新lrc播放器2》-iPhone上可以显示lrc歌词的播放器可以在播放mp3文件时显示lrc文件中的歌词的播放器

    https://apps.apple.com/cn/app/%E6%96%B0lrc%E6%92%AD%E6%94%BE%E5%99%A82/id1535214306 以前,在iPhone上播放lrc ...

  4. matlab 矩阵中的矩阵的特征值,当矩阵的所有条目都是变量时,如何在matlab中找出矩阵的特征值?...

    在MATLAB中没问题. >> syms a b c d e >> M = [a*b -c -d 0 -c e -a -b-d -d -a d -e 0 -b-d -e a]; ...

  5. [原创]Enterprise Architecture V7.5 C++代码生成时,头文件中函数声明没有注释,CPP中函数定义却有注释。...

    这几天一直在用Enterprise Architecture来抽象项目中要用到的一些数据结构和类,然后都做得差不多了之后发现,生成代码的时候.h文件中类成员函数部分没有注释,但是.cpp文件中的函数定 ...

  6. 23.C++类对象的指针为空时,调用成员函数不会挂掉

    最近工作的时候遇到了一个现象,当通过C++类对象的空指针调用没有使用this指针的成员函数时,不会出现段错误 测试代码 #include <iostream>using namespace ...

  7. qt使用自带的日志输出实例输出日志时,在日志中显示行数

    当使用qInstallMessageHandler()安装回调函数,通过回调函数来输出日志时,日志文件中没有行数和文件信息.可以在.pro文件中添加以下代码: #release中在日志添加行数,文件信 ...

  8. matlab输入指令错误怎么修改,在MATLAB中运行程序时,显示错误: 此上下文中不允许函数定义。 怎么修改?...

    点击查看在MATLAB中运行程序时,显示错误: 此上下文中不允许函数定义. 怎么修改?具体信息 答:MATLAB程序运行错误后,切换到MATLAB命令行中,观察命令行中的错误信息,确定错误原因. 1. ...

  9. 10.QT事件机制源码时序分析(中)

    接上一篇文章https://blog.csdn.net/Master_Cui/article/details/109162220,上篇文章已经说过,在Ubuntu18.04,QT的事件机制实际上是采用 ...

  10. Linux下程序崩溃dump时的 core文件的使用方法

    Linux下程序崩溃dump时的 core文件的使用方法 1.在启动程序前执行 ulimit -c unlimited unlimited 表示生成文件的大小限制,也可以修改为自定义的大小,例如: u ...

最新文章

  1. php 读取php.ini,php7 读取php.ini[4]
  2. php调用hive,如何进行hive的简单操作
  3. sublime3安装package controller遇到的问题
  4. Android开发之adb命令输入文本到手机输入框中的方法
  5. VC2010如何给ActiveX添加事件
  6. 第三十七期:刷脸支付叫好不叫座,为啥消费者和商家都不愿用先进科技?
  7. python正则_正则化方法及Python实现
  8. c语言双精度型输出小数位数_4.1 C语言数据的输出
  9. LINUX查看文件系统
  10. 2015第一弹:调试自己,挖掘自己的最强手艺
  11. win10底部任务栏无响应解决办法
  12. Elasticsearch之 cerebro 安装配置详细使用
  13. 微信小程序:智力考验看成语猜古诗句好玩解闷小游戏下载
  14. 作为前端你不得不知-浏览器的工作原理:网络浏览器幕后揭秘
  15. Cobaltstrike学习(二)beacon命令
  16. 中国剩余定理用python实现
  17. React生命周期(经典)
  18. 中柏平板bios对照表_BIOS中英文对照表(BIOS图解大全)
  19. 无法在计算机上创建文件夹iscsi,不会在 iSCSI 设备上重新创建文件共享 - Windows Server | Microsoft Docs...
  20. 苹果电池用量记录怎么清零

热门文章

  1. 您与此网站建立的连接不安全_CDN加速网站SEO优化,这就是CDN
  2. Jackson解析XML
  3. mysql表空间查看及创建
  4. 都昌信息袁永福:利用电子病历赋能框架,为健康医疗大数据打好基础【电子病历和健康医疗大数据系列】...
  5. 《Splunk智能运维实战》——2.8 列出浏览次数最多的产品
  6. 分布式之Zookeeper使用
  7. STC1_FULLSCREEN_TABLE_CONTROL
  8. 用好Windows 7自带文件加密工具
  9. 新人如何适应自己的领导
  10. Linux RedHat 5.2 构建PostFix邮件服务器