Loading

PIMPL

PIMPL(Pointer to Implementation)本质上也属于设计模式的一种,PIMPL也称为 Opaque Pointer(不透明的指针)。主要目的是将一个类的 实现细节(private/protected 方法、成员)其对外的公共接口 分离出来,使得 实现细节 可以在不影响客户端代码的情况下进行更改。

这样做的好处主要有以下两点:

  1. 对外隐藏实现细节,尤其是隐藏对外提供的库的实现细节
  2. 降低编译依赖,从而减少重新编译的时间

PIMPL实现的要点:

  1. 前向声明一个内部嵌套类Impl及其指针
  2. 将具体实现细节(private/protected 方法、成员)转移到内部嵌套类Impl中

隐藏实现细节

具体场景

在实际工作的过程中,需要对外提供一些库以及相应的头文件供三方人员使用,假设某个库的头文件如下:

// MyClass.h
#include <string>
#include <iostream>

class MyClass {
public:
    MyClass();
    ~Myclass();
    void publicMethod();
    
private:
    void privateMethod();
    
private:
    std::string mName_;
};

三方人员只需要使用到我们暴露的公共接口即可,但是我们却不得不将上述头文件提供给他们,这会导致如下两个问题:

  1. 实现细节被暴露,三方人员可以看到整个类的实现细节
  2. 接口不稳定,如果我们对mName_的类型进行改动,或者增加了一些成员变量,虽然没有修改public下的接口,但是对于使用人员来说也要更新头文件
  3. 增加了使用者的依赖,如2所说,使用者只关心自己调用的接口,不希望因为实现细节更改导致自己需要进行更改,显然当前提供的头文件无法满足

解决办法-PIMPL

PIMPL的引入就是为了解决上述问题,按照PIMPL的实现要点修改上述头文件如下:

// MyClass.h
#include <string>
#include <iostream>

class MyClass {
public:
    MyClass();
    ~Myclass();
    void publicMethod();
    
private:
    struct Impl;
    Impl* pImpl_;
};

对应的源文件如下:

// MyClass.cpp
#include "MyClass.h"

void Myclass::publicMethod() {
	std::cout << "public method" << std::endl;
    // 调用具体实现细节
    pImpl_->privateMethod;
}
struct Myclass::Impl {
	void privateMethod() {
		std::cout << "private method" << std::endl; 
	}
    
    std::string mName_;
}

Myclass::Myclass() 
:pImpl_(new Impl)
{}

Myclass::~Myclass() {
	delete pImpl_;
}

可以看出,修改后的代码 MyClass 的实现细节被封装在 MyClass::Impl 中,MyClass 的头文件只包含了公共接口;从而保证了:

  1. 不暴露实现细节
  2. 实现细节的改动不影响使用者,因为使用者的头文件中并不包含具体实现

降低编译依赖

降低编译依赖是由于编译器的工作方式以及C++的分离编译机制所决定的。在典型的C++项目中,每个源文件都会被独立地编译成目标文件(通常是.o.obj文件)。这些目标文件之后由链接器(linker)将它们组合成可执行文件或库。

所以当我们对源文件的修改不涉及头文件中提供的公共接口,只需要重新编译源文件,而不需要重新编译使用者的代码;

举例如下:

假设使用者的代码编译结果为clien.o, 当我们只修改了 MyClass.cpp 中的具体实现,例如增加了一个成员变量,而没有改变 MyClass.h 中的声明,那么只需要重新编译 MyClass.cpp 文件:

$ g++ -c MyClass.cpp -o MyClass.o

之后,你可以链接 Client.o 与新生成的 MyClass.o

$ g++ Client.o MyClass.o -o myProgram

这样就不必重新编译使用者的源代码Client.cpp,因为它只依赖于 MyClass.h 的声明,而不依赖于 MyClass.cpp 中的实现。

现代C++中的PIMPL

以上是传统C++中的PIMPL的实现,现在C++应尽量避免使用裸指针,而是使用智能指针。

所以修改后代码如下:

// MyClass.h
#include <string>
#include <iostream>
#include <memory>

class MyClass {
public:
    MyClass();
    ~Myclass();  // 只声明不实现,否则会报static_assert(sizeof(_Tp) > 0, "can't delete an incomplete type");错误
    void publicMethod();
    
private:
    struct Impl;
    std::unique_ptr<Impl> pImpl_;
};
// MyClass.cpp
#include "MyClass.h"

void Myclass::publicMethod() {
	std::cout << "public method" << std::endl;
    // 调用具体实现细节
    pImpl_->privateMethod;
}
struct Myclass::Impl {
	void privateMethod() {
		std::cout << "private method" << std::endl; 
	}
    
    std::string mName_;
}

Myclass::Myclass() 
:pImpl_(std::make_unique<Impl>())
{}

Myclass::~Myclass() = default

注意:上述代码中,如果没有显示声明未实现的析构函数,会导致报错,因为如果析构函数已经实现或者默认合成,编译时因为在析构函数实现时,无法知道Impl的具体实现细节,即Impl此时为不完全类型,会导致报错;这是因为 std::unique_ptr 的设计是在编译期检查确保在尝试删除指向不完全类型的指针时会产生错误

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

如果 _Tp 是不完全类型,其大小为零,编译器就会产生上述 static_assert 错误。

但是shared_ptr并不会要求_Tp是一个完整类型

posted @   Christopher_James  阅读(6)  评论(0编辑  收藏  举报
相关博文:
阅读排行:
· 阿里最新开源QwQ-32B,效果媲美deepseek-r1满血版,部署成本又又又降低了!
· Manus重磅发布:全球首款通用AI代理技术深度解析与实战指南
· 开源Multi-agent AI智能体框架aevatar.ai,欢迎大家贡献代码
· 被坑几百块钱后,我竟然真的恢复了删除的微信聊天记录!
· AI技术革命,工作效率10个最佳AI工具
点击右上角即可分享
微信分享提示