特殊函数与 Pimpl Idiom
发布: (2026年3月7日 GMT+8 07:30)
4 分钟阅读
原文: Dev.to
Source: Dev.to
好处
这些好处体现在降低编译时间和更简便的头文件管理上。
编译时间会减少,因为我们不再需要在头文件中声明每个成员(这会迫使我们包含相应的头文件),而是把成员隐藏在一个指针后面。例如,如果我们有 std::vector 和 std::string 类型的成员,通常需要包含它们的头文件。对这些成员类型——或对经常修改的自定义类型——的任何更改,都会导致整个类的头文件重新编译。
使用指针
顾名思义,我们持有指向实现的指针。该指针可以是原始指针、std::unique_ptr 或 std::shared_ptr。
原始指针和 unique_ptr 在性能上最友好,所以我们先来看这两种情况。
指针
// Header file
class Widget
{
public:
Widget();
~Widget();
private:
struct Impl; // forward declaration (incomplete type)
Impl* pImpl; // pointer to implementation
};// Source file
struct Widget::Impl
{
std::vector m_age;
std::string m_name;
PlayerController m_controller; // custom type
};
Widget::Widget()
{
// Constructor
pImpl = new Impl();
}
Widget::~Widget()
{
delete pImpl;
}独占指针(Unique Pointer)
在深入代码之前,先说明一下 Pimpl 习语的几个基本要点:
- 它隐藏私有数据,使公共 API 保持稳定。
- 头文件只包含一个小指针(通常是 4 或 8 字节),因此实现的更改不会强制重新编译头文件。
- 间接访问会带来轻微的运行时开销。
- 定义拷贝操作时需要格外小心。
这种做法非常适合拥有大量私有成员的类。
// Header file
class Person
{
public:
Person(std::string name, std::string address);
~Person();
// Copy operations
Person(const Person& rhs);
Person& operator=(const Person& rhs) = delete;
// Move operations
Person(Person&& rhs);
Person& operator=(Person&& rhs) = delete;
std::string GetAttributes() const;
private:
struct Impl;
std::unique_ptr pImpl;
};// Source file
// Define the structure that holds the member data
struct Person::Impl
{
std::string name;
std::string address;
};
Person::Person(std::name, std::address) : pImpl{std::make_unique()}
{
pImpl->name = name;
pImpl->address = address;
}
Person::~Person() = default;
Person::Person(const Person& rhs)
{
pImpl = std::make_unique();
pImpl->name = rhs.pImpl->name;
pImpl->address = rhs.pImpl->address;
}
Person::Person(Person&& rhs) noexcept
{
pImpl = std::move(rhs.pImpl);
}
std::string Person::GetAttributes() const
{
return pImpl->name + '\t' + pImpl->address;
}需要注意的几点
- 实现结构体必须在任何使用它的构造函数之前定义;否则编译器会报错,因为
std::unique_ptr不能管理不完整的类型。 - 声明析构函数会抑制编译器生成的移动操作,所以如果需要必须自行定义。
- 使用独占指针时需要提供自定义拷贝构造函数;默认的拷贝会进行浅拷贝。
共享指针
在 Pimpl 习语中使用 std::shared_ptr 较少见,但它可以省去编写自定义拷贝或移动操作的工作,因为共享指针本身就是可拷贝且可移动的。它同样支持不完整类型,所以实现结构体可以在头文件中仅做前向声明。