命名空间
变体
操作

PImpl

来自 cppreference.cn
< cpp‎ | language
 
 
C++ 语言
通用主题
流程控制
条件执行语句
if
迭代语句 (循环)
for
范围 for (C++11)
跳转语句
函数
函数声明
Lambda 函数表达式
inline 说明符
动态异常规范 (在 C++17 中弃用*)
noexcept 说明符 (C++11)
异常
命名空间
类型
说明符
const/volatile
decltype (C++11)
auto (C++11)
constexpr (C++11)
consteval (C++20)
constinit (C++20)
存储期说明符
初始化
 

“指向实现”或 “pImpl” 是一种 C++ 编程技术,它通过将类的实现细节放置在一个单独的类中,并通过不透明指针访问它们,从而从类的对象表示中移除实现细节

// --------------------
// interface (widget.h)
struct widget
{
    // public members
private:
    struct impl; // forward declaration of the implementation class
    // One implementation example: see below for other design options and trade-offs
    std::experimental::propagate_const< // const-forwarding pointer wrapper
        std::unique_ptr<                // unique-ownership opaque pointer
            impl>> pImpl;               // to the forward-declared implementation class
};
 
// ---------------------------
// implementation (widget.cpp)
struct widget::impl
{
    // implementation details
};

此技术用于构建具有稳定 ABI 的 C++ 库接口,并减少编译时依赖性。

目录

[编辑] 解释

由于类的私有数据成员参与其对象表示,影响大小和布局,并且由于类的私有成员函数参与重载解析 (这发生在成员访问检查之前),因此对这些实现细节的任何更改都需要重新编译该类的所有用户。

pImpl 消除了这种编译依赖性;对实现的更改不会导致重新编译。因此,如果库在其 ABI 中使用 pImpl,则库的较新版本可以更改实现,同时保持与旧版本 ABI 兼容。

[编辑] 权衡

pImpl 惯用法的替代方案是

  • 内联实现:私有成员和公共成员是同一类的成员。
  • 纯抽象类 (OOP 工厂):用户获取指向轻量级或抽象基类的唯一指针,实现细节位于覆盖其虚成员函数的派生类中。

[编辑] 编译防火墙

在简单情况下,pImpl 和工厂方法都消除了实现与类接口用户之间的编译时依赖性。工厂方法创建了对虚函数表 (vtable) 的隐藏依赖,因此重新排序、添加或删除虚成员函数会破坏 ABI。pImpl 方法没有隐藏的依赖关系,但是如果实现类是类模板特化,则编译防火墙的好处会丢失:接口的用户必须观察整个模板定义才能实例化正确的特化。在这种情况下,常见的设计方法是以避免参数化的方式重构实现,这是 C++ 核心指南的另一个用例

例如,以下类模板在其私有成员或 push_back 的主体中不使用类型 T

template<class T>
class ptr_vector
{
    std::vector<void*> vp;
public:
    void push_back(T* p)
    {
        vp.push_back(p);
    }
};

因此,私有成员可以按原样转移到实现,并且 push_back 可以转发到接口中也不使用 T 的实现

// ---------------------
// header (ptr_vector.hpp)
#include <memory>
 
class ptr_vector_base
{
    struct impl; // does not depend on T
    std::unique_ptr<impl> pImpl;
protected:
    void push_back_fwd(void*);
    void print() const;
    // ... see implementation section for special member functions
public:
    ptr_vector_base();
    ~ptr_vector_base();
};
 
template<class T>
class ptr_vector : private ptr_vector_base
{
public:
    void push_back(T* p) { push_back_fwd(p); }
    void print() const { ptr_vector_base::print(); }
};
 
// -----------------------
// source (ptr_vector.cpp)
// #include "ptr_vector.hpp"
#include <iostream>
#include <vector>
 
struct ptr_vector_base::impl
{
    std::vector<void*> vp;
 
    void push_back(void* p)
    {
        vp.push_back(p);
    }
 
    void print() const
    {
        for (void const * const p: vp) std::cout << p << '\n';
    }
};
 
void ptr_vector_base::push_back_fwd(void* p) { pImpl->push_back(p); }
ptr_vector_base::ptr_vector_base() : pImpl{std::make_unique<impl>()} {}
ptr_vector_base::~ptr_vector_base() {}
void ptr_vector_base::print() const { pImpl->print(); }
 
// ---------------
// user (main.cpp)
// #include "ptr_vector.hpp"
 
int main()
{
    int x{}, y{}, z{};
    ptr_vector<int> v;
    v.push_back(&x);
    v.push_back(&y);
    v.push_back(&z);
    v.print();
}

可能的输出

0x7ffd6200a42c
0x7ffd6200a430
0x7ffd6200a434

[编辑] 运行时开销

  • 访问开销:在 pImpl 中,每次调用私有成员函数都会通过指针间接访问。私有成员对公共成员的每次访问都会通过另一个指针间接访问。这两种间接访问都跨越了翻译单元边界,因此只能通过链接时优化来消除。请注意,OO 工厂需要跨翻译单元的间接访问才能访问公共数据和实现细节,并且由于虚函数分派,为链接时优化器提供的机会更少。
  • 空间开销:pImpl 向公共组件添加一个指针,并且如果任何私有成员需要访问公共成员,则向实现组件添加另一个指针,或者将其作为参数传递给每个需要它的私有成员的调用。如果支持有状态的自定义分配器,则还必须存储分配器实例。
  • 生命周期管理开销:pImpl(以及 OO 工厂)将实现对象放在堆上,这在构造和析构时会带来显著的运行时开销。这可以通过自定义分配器部分抵消,因为 pImpl(但不是 OO 工厂)的分配大小在编译时是已知的。

另一方面,pImpl 类是移动友好的;将大型类重构为可移动的 pImpl 可能会提高操作包含此类对象的容器的算法的性能,尽管可移动的 pImpl 具有额外的运行时开销来源:任何允许在已移动对象上使用的公共成员函数,并且需要访问私有实现,都会产生空指针检查。

[编辑] 维护开销

pImpl 的使用需要专用的翻译单元(仅标头库不能使用 pImpl),引入了一个额外的类、一组转发函数,并且如果使用分配器,则在公共接口中暴露了分配器使用的实现细节。

由于虚成员是 pImpl 的接口组件的一部分,因此模拟 pImpl 意味着仅模拟接口组件。可测试的 pImpl 通常被设计为允许通过可用的接口进行完整的测试覆盖。

[编辑] 实现

由于接口类型的对象控制实现类型的对象的生命周期,因此指向实现的指针通常是 std::unique_ptr

因为 std::unique_ptr 要求指向的类型在实例化删除器的任何上下文中都是完整类型,所以特殊成员函数必须是用户声明的,并且在实现文件中进行外部定义,在该文件中实现类是完整的。

因为当 const 成员函数通过非 const 成员指针调用函数时,会调用实现函数的非 const 重载,所以指针必须包装在 std::experimental::propagate_const 或等效物中。

所有私有数据成员和所有私有非虚成员函数都放置在实现类中。所有公共、受保护和虚成员都保留在接口类中(有关替代方案的讨论,请参阅 GOTW #100)。

如果任何私有成员需要访问公共或受保护成员,则可以将接口的引用或指针作为参数传递给私有函数。或者,反向引用可以作为实现类的一部分来维护。

如果打算为实现对象的分配支持非默认分配器,则可以使用任何常用的分配器感知模式,包括分配器模板参数默认为 std::allocatorstd::pmr::memory_resource* 类型的构造函数参数。

[编辑] 注释

[编辑] 示例

演示了带有 const 传播、作为参数传递的反向引用、没有分配器感知以及在没有运行时检查的情况下启用移动的 pImpl

// ----------------------
// interface (widget.hpp)
#include <experimental/propagate_const>
#include <iostream>
#include <memory>
 
class widget
{
    class impl;
    std::experimental::propagate_const<std::unique_ptr<impl>> pImpl;
public:
    void draw() const; // public API that will be forwarded to the implementation
    void draw();
    bool shown() const { return true; } // public API that implementation has to call
 
    widget(); // even the default ctor needs to be defined in the implementation file
              // Note: calling draw() on default constructed object is UB
    explicit widget(int);
    ~widget(); // defined in the implementation file, where impl is a complete type
    widget(widget&&); // defined in the implementation file
                      // Note: calling draw() on moved-from object is UB
    widget(const widget&) = delete;
    widget& operator=(widget&&); // defined in the implementation file
    widget& operator=(const widget&) = delete;
};
 
// ---------------------------
// implementation (widget.cpp)
// #include "widget.hpp"
 
class widget::impl
{
    int n; // private data
public:
    void draw(const widget& w) const
    {
        if (w.shown()) // this call to public member function requires the back-reference 
            std::cout << "drawing a const widget " << n << '\n';
    }
 
    void draw(const widget& w)
    {
        if (w.shown())
            std::cout << "drawing a non-const widget " << n << '\n';
    }
 
    impl(int n) : n(n) {}
};
 
void widget::draw() const { pImpl->draw(*this); }
void widget::draw() { pImpl->draw(*this); }
widget::widget() = default;
widget::widget(int n) : pImpl{std::make_unique<impl>(n)} {}
widget::widget(widget&&) = default;
widget::~widget() = default;
widget& widget::operator=(widget&&) = default;
 
// ---------------
// user (main.cpp)
// #include "widget.hpp"
 
int main()
{
    widget w(7);
    const widget w2(8);
    w.draw();
    w2.draw();
}

输出

drawing a non-const widget 7
drawing a const widget 8

[编辑] 外部链接

1.  GotW #28 : 快速 Pimpl 惯用法。
2.  GotW #100: 编译防火墙。
3.  Pimpl 模式 - 你应该知道的。