首页
学习
活动
专区
工具
TVP
发布
精选内容/技术社群/优惠产品,尽在小程序
立即前往

PImpl

“指向实现的指针”或“PIMPL”是一种C++编程技术。[1]它通过将类放置在单独的类中,通过不透明指针访问,从而从其对象表示中删除类的实现细节:

二次

代码语言:javascript
复制
// widget.h (interface)
class 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
};
 
// widget.cpp (implementation)
struct widget::impl {
   // implementation details
};

二次

该技术用于构造具有稳定ABI的C++库接口,减少编译时依赖关系。

解释

因为类的私有数据成员参与其对象表示,影响了大小和布局,而且因为类的私有成员函数参与了过载分辨率%28发生在成员访问检查%29之前,对这些实现细节的任何更改都需要重新编译类的所有用户。

PIMPL打破了这种编译依赖关系;对实现的更改不会导致重新编译。因此,如果库在其ABI中使用PIMPL,则库的更新版本可能会更改实现,同时保持ABI与旧版本兼容。

权衡

PIMPL成语的替代方法是。

  • 内联实现:私有成员和公共成员是同一类的成员。
  • 纯抽象类%28 OOP工厂%29:用户获得指向轻量级或抽象基类的唯一指针,实现详细信息在重写其虚拟成员函数的派生类中。

编译防火墙

在简单的情况下,PIMPL和工厂方法都打破了类接口的实现和用户之间的编译时依赖关系。工厂方法在vtable上创建一个隐藏的依赖项,因此重新排序、添加或删除虚拟成员函数会破坏ABI。PIMPL方法没有隐藏的依赖关系,但是,如果实现类是类模板专门化,则编译防火墙的好处将丢失:接口的用户必须观察整个模板定义,才能实例化正确的专门化。在这种情况下,一种常见的设计方法是以避免参数化的方式重构实现,这是C++核心指南的另一个用例T.61.不要过分地将成员参数化使用非模板核心实现来提供abi稳定的接口。...

例如,下面的类模板在其私有成员或推送体中不使用类型T[医]背

二次

代码语言:javascript
复制
template<class T>
class ptr_vector {
    void **beg, **end, **cap;
public:
    void push_back(T* p) {
        if (end == cap) reallocate(end - beg + 1);
        *end++ = p;
    }
};

二次

因此,私人成员可以按原样进行执行,并推动。[医]Back可以转发到在接口中不使用T的实现:

二次

代码语言:javascript
复制
// header (ptr_vector.h)
class ptr_vector_base {
    struct impl; // does not depend on T
    std::unique_ptr<impl> pImpl;
 protected:
    void push_back_fwd(void*);
    ... // see implementation section for special member functions
};
template<class T>
class ptr_vector : private ptr_vector_base {
public:
    void push_back(T* p) { push_back_fwd(p); }
};
 
// source (ptr_vector.cpp)
struct ptr_vector_base::impl {
    void **beg, **end, **cap;
    void push_back(void* p) {
        if (end == cap) reallocate(end - beg + 1);
        *end++ = p;
    }
    void reallocate(size_t sz) { ... }
};
void ptr_vector_base::push_back_fwd(void* p) { pImpl->push_back(p); }
ptr_vector_base::ptr_vector_base() : pImpl(std::make_unique<impl>()) {}

二次

运行时开销

  • 访问开销:在PIMPL中,每个对私有成员函数的调用都是通过一个指针进行的。私有成员对公共成员的每次访问都是通过另一个指针进行的。双向交叉平移单元边界等只能通过链接时间优化来优化.。请注意,OO工厂需要跨翻译单元间接访问公共数据和实现细节,并且由于虚拟调度而为链接时间优化器提供的机会更少。
  • 空间开销:PIMPL向公共组件添加一个指针,如果任何私有成员需要访问公共成员,则另一个指针被添加到实现组件中,或者作为参数传递给每个需要它的私有成员。如果支持有状态自定义分配器,则还必须存储分配器实例。
  • 生存期管理开销:PIMPL%28 AS和OO工厂%29将实现对象放置在堆中,这会在构建和销毁时增加大量的运行时开销。这可能被自定义分配器部分抵消,因为在编译时知道PIMPL%28而不是OO工厂%29的分配大小。

另一方面,PIMPL类是移动友好的;将一个大类重构为可移动的PIMPL可能会提高操纵容器的算法的性能。虽然可移动的PIMPL还有一个额外的运行时开销来源:任何允许从移出对象上使用的公共成员函数以及对私有实现的访问都会导致空指针检查。

维修费用

使用PIMPL需要一个专门的翻译单元%28a头库不能使用PIMPL%29,引入一个额外的类,一组转发函数,如果使用分配器,则公开在公共接口中使用分配器的实现细节。

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

实施

由于接口类型的对象控制实现类型对象的生存期,所以指向实现的指针通常是std::unique_ptr...

因为std::unique_ptr要求指向类型在调用删除器的任何上下文中都是一个完整类型,析构函数和复制/移动赋值运算符必须在实现类完成的实现文件中以用户声明和定义的方式出线。这将取消移动构造函数,必须定义或默认该构造函数。

因为当const成员函数通过非const成员指针调用一个函数时,调用实现函数的非const重载,因此必须将指针包装在std::experimental::propagate_const或者同等的。

所有私有数据成员和所有私有非虚拟成员函数都放在实现类中。所有公共成员、受保护成员和虚拟成员都保留在接口类%28中(参见GOTW#100用于讨论替代品%29。

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

如果要支持非默认分配器来分配实现对象,则可以使用任何常见的分配器识别模式,包括默认为std::allocator类型的构造函数参数std::pmr::memory_resource*...

演示具有Const传播的PIMPL,该PIMPL以参数形式传递,无需分配器识别,且无需运行时检查就启用移动。

二次

代码语言:javascript
复制
#include <iostream>
#include <memory>
#include <experimental/propagate_const>
 
// interface (widget.h)
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(int);
    ~widget(); // defined in the implementation file, where impl is a complete type
    widget(widget&&) = default;  // 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)
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(int n) : pImpl{std::make_unique<impl>(n)} {}
widget::~widget() = default;
widget& widget::operator=(widget&&) = default;
 
// user (main.cpp)
int main()
{
    widget w(7);
    const widget w2(8);
    w.draw();
    w2.draw();
}

二次

产出:

二次

代码语言:javascript
复制
drawing a non-const widget 7
drawing a const widget 8

二次

注记

参考文献

  1. GotW#100*汇编防火墙
代码语言:txt
复制
 ? cppreference.com

在CreativeCommonsAttribution下授权-ShareAlike未移植许可v3.0。

扫码关注腾讯云开发者

领取腾讯云代金券

http://www.vxiaotou.com