C++语言导学(4): pImpl习惯用法¶
条款31: 将文件间的编译依存关系降到最低。 --- 《Effective C++》
条款22: 使用pImpl习惯用法时,将特殊成员函数的定义放到实现文件中。 --- 《Effective Modern C++》
pImpl
习惯用法,即pointer to implementation
,指的是以指针的方式访问类的实现部分。
比如:
在这个类定义中,因为数据成员的缘故,这个头文件里面需要包含string,vector等,进而导致凡是使用了这个类的地方都会需要编译一次。
这不但提高了整体的编译时间,而且因为依赖问题一旦有任何改变都需要重新编译一次。
pImpl习惯用法可以降低依赖以提升编译速度,如:
// Widget.hpp
class Widget{
public:
Widget();
~Widget();
//...
private:
struct Impl; // 用一个结构体来存储实现部分
Impl *pImpl; // 用一个指针来访问实现部分
};
Widget::Impl是一个内部的非完整类型,而这里的技巧就在于用一个指针来指向它,从而不需要包含string等头文件,也不会因为依赖问题导致跟随着重新编译。
在实现部分来处理类的成员:
// Widget.cpp
struct Widget::Impl
{
std::string name;
std::vector<double> data;
};
Widget()
: pImpl(new Impl)
{
}
~Widget()
{
delete pImpl;
}
// main.cpp
#include "Widget.hpp"
int main()
{
Widget w;
return 0;
}
但我们想使用智能指针来处理,而不是直接使用“裸”指针,也不想出现new和delete语句:
// Widget.hpp
#include <memory>
class Widget {
public:
Widget();
private:
struct Impl;
std::unique_ptr<Impl> pImpl;
};
// Widget.cpp
#include "Widget.hpp"
#include <string>
#include <vector>
struct Widget::Impl
{
std::string name;
std::vector<double> data;
};
Widget::Widget()
: pImpl(std::make_unique<Impl>())
{
}
但因为使用了unique_ptr智能指针,它会因为Impl在头文件中是一个非完整类型而报错,原因是编译器自动产生的一些默认代码时还不知道Impl的完整定义,需要做些改动:
// Widget.hpp
#include <memory>
class Widget {
public:
Widget();
~Widget(); // 不使用编译器自动生成的析构函数
private:
struct Impl;
std::unique_ptr<Impl> pImpl;
};
// Widget.cpp
#include "Widget.hpp"
#include <string>
#include <vector>
struct Widget::Impl
{
std::string name;
std::vector<double> data;
};
Widget::Widget()
: pImpl(std::make_unique<Impl>())
{
}
Widget::~Widget() = default; // 还是使用编译自动产生的默认析构函数,但此时已经知道Impl的完整定义了。
这样就可以顺利编译了。
但因为unique_ptr要求类型是完整类型,相应的需要把一些特殊函数进行声明和实现,这样才能正确执行移动、复制等操作:
// Widget.hpp
#include <memory>
class Widget {
public:
Widget();
~Widget(); // 析构函数
Widget(Widget&& rhs); // 移动构造函数
Widget& operator=(Widget&& rhs); // 移动复制运算符
Widget(const Widget& rhs); // 复制构造函数
Widget& operator=(const Widget& rhs); // 复制赋值运算符
private:
struct Impl;
std::unique_ptr<Impl> pImpl;
};
// Widget.cpp
#include "Widget.hpp"
#include <string>
#include <vector>
struct Widget::Impl
{
std::string name;
std::vector<double> data;
};
Widget::Widget()
: pImpl(std::make_unique<Impl>())
{
}
Widget::~Widget() = default;
Widget::Widget(Widget&& rhs) = default;
Widget& Widget::operator=(Widget&& rhs) = default;
Widget::Widget(const Widget& rhs)
: pImpl(std::make_unique<Impl>(*rhs.pImpl))
{
}
Widget& Widget::operator=(const Widget& rhs)
{
*pImpl = *rhs.pImpl;
return *this;
}
测试:
// main.cpp
#include "Widget.hpp"
int main()
{
Widget w;
Widget w1(w);
Widget w2 = w;
auto w3(std::move(w));
auto w4 = std::move(w3);
return 0;
}
基本上在采用pImpl习惯用法时,首选使用unique_ptr智能指针,进而必须实现类的五个特殊函数:
- 析构函数
- 移动构造函数
- 移动赋值运算符
- 复制构造函数
- 复制赋值运算符
采用pImpl习惯用法不但加快编译速度,而且做到接口和实现的真正分离,尽可能采用此用法。
- 微信搜索: 「 MinYiLife 」, 关注公众号!
- 本文链接: https://www.lesliezhu.com/blog/2022/10/25/cpp_4/
- 版权声明: 原创文章,如需转载请注明文章作者和出处。谢谢!