1.pImpl技术
1.1 原理介绍
指向实现对象的指针(Pointer to implementation, PImp)是一项C++编程技术,通过将类实现细节放置在单独类中(内部通过不透明指针访问),进而实现将类实现细节从类声明中移除。
通过这项技术就可以实现:
- (1) 声明与定义分离,对用户隐藏实现;
- (2) 加快编译速度:原始实现中,当需要改变成员变量时,所有包含该头文件的源文件都需要重新编译,现在只需要去实现文件中进行修改,从这个层面上来讲,加快了编译速度;
1.2 实现方法
1.2.1 一般实现
- 头文件代码:
// A.h
#pragma once
#include <string>
#include <memory>
class A {
public:
A();
~A();
double getData(int index) const;
std::string getName() const;
private:
struct Impl;
std::unique_ptr<Impl> pimpl_;
};
- 实现代码
// A.cc
#include "A.h"
#include <string>
#include <vector>
struct A::Impl {
std::string name_ = "mirror";
std::vector<double> data_ = { 1.0, 2.0, 3.0};
};
A::A() {
pimpl_.reset(new Impl());
}
double A::getData(int index) const {
if (index > 0 && index < pimpl_->data_.size()) {
return pimpl_->data_[index];
}
return 0;
}
std::string A::getName() const {
return pimpl_->name_;
}
A::~A() = default;
- 当A的析构函数实现直接写在A.h中时,会出现如下错误
required from 'std::unique_ptr<_Tp, _Dp>::~unique_ptr() [with _Tp = A::Impl; _Dp = std::default_delete<A::Impl>]'
error: invalid application of 'sizeof' to incomplete type 'A::Impl'
static_assert(sizeof(_Tp)>0
- 原因:A析构函数实现在头文件时,Impl对于A的析构函数而言只有声明,实现则是不可见的,因此并不知道Impl有哪些成员,也不知道Impl类占用多大内存,所以sizeof(Impl)时报错。
1.2.2 更一般的实现
- 上面那种实现还是需要前置声明实现类Impl,这里可以用一种更常见的做法,利用C语言的void*万能指针来实现:
- 头文件代码
#pragma once
#include <string>
class A {
public:
A();
~A();
double getData(int index) const;
std::string getName() const;
private:
void *pimpl_;
};
- 实现代码
// A.cc
#include "A.h"
#include <string>
#include <vector>
struct Impl {
std::string name_ = "mirror";
std::vector<double> data_ = { 1.0, 2.0, 3.0};
};
A::A() {
pimpl_ = new Impl();
}
double A::getData(int index) const {
if (index > 0 && index < pimpl_->data_.size()) {
return pimpl_->data_[index];
}
return 0;
}
std::string A::getName() const {
return pimpl_->name_;
}
A::~A() {
Impl *ptr = ( Impl * )pImpl_;
delete ptr;
ptr = nullptr;
}
1.3 技术优缺点
优点:
- (1) 隔离内外,形成一个安全区,将错误控制在设计范围内;
- (2) 减少二义性,隐藏意味着外部调用产生二义性的可能性被尽量隔绝;
- (3) 头文件依赖减少,带来编译开销降低;
- (4) 对ABI(Application Binary Interface)有更好的兼容性;
- (5) 有可能使用延迟加载,提高资源利用率;
缺点:
- (1) 增加复杂性,多一层抽象就多一层效率耗减,同时对指针管理也增加了复杂性;
- (2) 需要处理拷贝(要么禁止掉);
- (3) 编译器不能跨过
pImpl
指针看到实现类内部细节,不能强制const
约束,因此存在const脱离了编译器掌控。当使用pImpl模式时,成员函数即使被声明为const
,它仍然可以通过pImpl
指针调用实现类中非const
成员函数,从而潜在地修改对象的状态。