3

pImpl 技巧:接口与实现分离

 3 years ago
source link: https://netcan.github.io/2020/03/12/pImpl%E6%8A%80%E5%B7%A7%EF%BC%9A%E6%8E%A5%E5%8F%A3%E4%B8%8E%E5%AE%9E%E7%8E%B0%E5%88%86%E7%A6%BB/
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.
neoserver,ios ssh client

pImpl 技巧:接口与实现分离

2020.03.12

Netcan

编程

 热度 88℃

最近在实现某个模块加强了对 pImpl 设计模式的理解,这里记录下来备忘。

pImpl模式用来将接口与实现分离,更改实现的话只需要重新编译 cpp 实现,不影响依赖其抽象的模块,也就是著名的桥接模式。

然而在实践中发现有很多类嵌套类,这种也算是 pImpl 的体现,但是做的不够完美,因为这些嵌套类直接定义在头文件中,头文件需要暴露出去,其他模块自然也会看到这些内部嵌套类的定义,影响依赖关系。比如:

// Module.h
class Module {
public:
    ...
private:
    class InternalModule { // Module 部分实现委托到 InternalModule 内部类
        ...
    } m_module;
};

这个模式核心所在是在头文件通过前向声明class Impl,并通过指针指向实例,然后在 cpp 中定义并实现Impl

// Person.h
#include <memory>
class Person {
public:
    Person(std::string name); // (1)
    ~Person(); // (2)
    void Dosomething();
    ...
private:
    struct Impl; // (3)
    std::unique_ptr<Impl> pImpl; // (4)
};
// Person.cpp
#include "Person.h"
class Person::Impl { // impl 定义与实现
public:
    Impl(std::string name): name(name) { }
    void Dosomething() {
        // ...
    }
private:
    std::string name;
};
void Person::Dosomething() {
    return pImpl->Dosomething();
}
// 构造、析构函数必须放在实现 cpp 里,避免 inline constructor/destructor 导致的 incomplete type `Impl' 问题
Person::Person(std::string name):
    pImpl(new Impl{name})  // 编程规范问题,可以放到 Init()函数中初始化
    { }
Person::~Person() = default;

需要注意几个点,由于采用 std::unqiue_ptr 托管 pImpl,而std::unqiue_ptr 的声明如下:

template <typename _Tp, typename _Dp = default_delete<_Tp>>
class unique_ptr;

其中 default_delete 模板实例化需要看到 Impl 的定义,而其定义在 cpp 中,导致构造、析构函数必须得放到 cpp 中:

  • (1) 构造函数需要在头文件中声明,cpp 中实现,而初始化 pImpl 可以放到 Init 函数中
  • (2) 析构函数同上
  • (3) 在类内前向声明Impl,避免暴露实现出去
  • (4) 使用 std::unique_ptr 托管pImpl

About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK