智能指针与Impl惯用法
不使用impl惯用法会怎样?
class widget
{
public:
widget();
~widget();
private:
std::string name;
std::vector<int> data;
};
- string和vector的头文件必须在声明widget的头文件中声明
- 前述头文件增加了widget用户的编译时间
- widget的客户也依赖于第一步中的头文件,如果某个头文件中的内容发生了变化,则widget的客户必须重新编译
impl惯用法如何实现?
#pragma once
struct Impl;
class widget
{
public:
widget();
~widget();
private:
struct Impl *pImpl;
};
#include "widget.h"
#include <string>
#include <vector>
#include "Gadget.h"
struct widget::Impl {
std::string name;
std::vector<double> data;
Gadget g;
};
widget::widget()
:pImpl(new Impl())
{
}
widget::~widget()
{
delete pImpl;
}
- 没有了上述性能问题,但是使用陈旧的C++98语法
使用std::unique_ptr
#pragma once
#include <memory>
class widget
{
public:
widget();
~widget();
widget(const widget& rhs);
widget &operator=(const widget& rhs);
widget(widget &&rhs);
widget &operator=(widget &&rhs);
private:
struct Impl;
std::unique_ptr<Impl> pImpl;
};
#include "widget.h"
#include <string>
#include <vector>
#include "Gadget.h"
struct widget::Impl {
std::string name;
std::vector<double> data;
Gadget g;
};
widget::widget()
:pImpl(std::make_unique<Impl>())
{
}
widget::~widget() = default;
widget::widget(const widget & rhs)
:pImpl(std::make_unique<Impl>(*rhs.pImpl))
{
}
widget &widget::operator=(const widget& rhs)
{
*pImpl = *rhs.pImpl;
return *this;
}
widget::widget(widget && rhs) = default;
widget &widget::operator=(widget &&rhs) = default;
- 注意我们声明了一个空的析构函数,这是必须的。原因:对象作用域结束后,对象析构->析构unique_ptr->static_assert(用来保证指针类型为完整类型)->delete,位于static_cast的时候需要保证p是完整类型。
- 注意在显式文件中的析构函数的写法,该写法表达了编译期生成的析构函数会做正确的事情,而声明它的唯一理由只是想要使得其定义出现在Widget的实现文件中。
- 我们显式声明了移动操作,原因是在widget中声明的析构函数会阻止编译器生成移动操作,并且移动操作同样需要看到完整类型定义(需要在实现文件中定义)。
a.编译器的移动赋值操作需要在重新赋值前析构pImpl指向的对象
b.编译器会在移动构造函数内抛出异常的时候生成析构pImpl的代码,而pImpl的析构函数要求Impl具有完整类型 - 我们显式定义了自己的拷贝构造函数以及重写了赋值运算符函数(std::make_unique(Impl impl)是值拷贝),因为由于std::unique_ptr的存在,编译器不会声明默认版本
使用unique_ptr还是shared_ptr
在不进行资源共享的前提下,为达到实现PImpl惯用法的目的,应该选择使用std::unique_ptr智能指针,因为对象内部的pImpl指针拥有相应实现对象的专属所有权。(含义清晰,但是编码复杂)
#pragma once
#include <memory>
class widget
{
public:
widget();
widget(const widget& rhs);
widget &operator=(const widget& rhs);
private:
struct Impl;
std::shared_ptr<Impl> pImpl;
};
#include "widget.h"
#include <string>
#include <vector>
#include "Gadget.h"
struct widget::Impl {
std::string name;
std::vector<double> data;
Gadget g;
};
widget::widget()
:pImpl(std::make_shared<Impl>())
{
}
widget::widget(const widget & rhs)
:pImpl(std::make_shared<Impl>(*rhs.pImpl))
{
}
widget &widget::operator=(const widget& rhs)
{
*pImpl = *rhs.pImpl;
return *this;
}
为什么同样是智能指针却有如此大的差别?
- 对std::unique_ptr而言,析构器类型是智能指针类型的一部分,这会使得编译器产生更小尺寸的运行期数据结构以及更快的运行期代码,带来的后果就是,要是使用编译器生成的特殊函数(例如拷贝或者移动)就要求器指向的类型必须是完整类型。
- 对std::shared_ptr而言,析构器的类型并不是智能指针的一部分,这会使得编译器产生更大更慢的代码。但是,获得的好处是在使用编译器生成的特殊函数时,其指向的类型不要求时完整类型
总结
- 在使用Pimpl习惯用法时,将特殊成员函数的定义放在实现文件中(使用shared_ptr的情形除外)
- 对于不完整类型(没有完整定义,只有类型声明)我们可以声明他的指针,这是Pimpl用法的实现前提
- Pimpl惯用法通过降低类的客户和类实现者之间的依赖性,减少了构建时间
- 对于采用std::unique_ptr来实现的pImpl指针,需要 在类的头文件中声明特种成员函数,但是在实现文件中实现他们,及时默认函数实现有正确的行为也必须这样做