特殊函数与 Pimpl Idiom

发布: (2026年3月7日 GMT+8 07:30)
4 分钟阅读
原文: Dev.to

Source: Dev.to

好处

这些好处体现在降低编译时间和更简便的头文件管理上。
编译时间会减少,因为我们不再需要在头文件中声明每个成员(这会迫使我们包含相应的头文件),而是把成员隐藏在一个指针后面。例如,如果我们有 std::vectorstd::string 类型的成员,通常需要包含它们的头文件。对这些成员类型——或对经常修改的自定义类型——的任何更改,都会导致整个类的头文件重新编译。

使用指针

顾名思义,我们持有指向实现的指针。该指针可以是原始指针、std::unique_ptrstd::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 较少见,但它可以省去编写自定义拷贝或移动操作的工作,因为共享指针本身就是可拷贝且可移动的。它同样支持不完整类型,所以实现结构体可以在头文件中仅做前向声明。

0 浏览
Back to Blog

相关文章

阅读更多 »