Pimpl(也叫"编译防火墙""Opaque Pointer",有时被误称为桥接模式)是一种用来隐藏类实现细节、减少编译依赖、稳定二进制接口(ABI)的技巧。核心做法:在头文件中只声明接口,把真实实现放到一个私有的实现类(Impl)里,通过指针持有。
1、为什么用 Pimpl
- 减少编译依赖与时间:头文件更轻,不需要包含庞大依赖。
- 隐藏实现细节:对外只暴露接口,内部结构可自由变化。
- 稳定 ABI:成员改动不影响类的大小和布局,降低重编译与二进制不兼容。
- 更好的封装:实现细节集中在 .cpp 中管理。
2、如何使用(要点)
- 在头文件前向声明
Impl,用std::unique_ptr<Impl>作为私有成员。 - 在 .cpp 中定义
Impl的具体内容,并实现接口转发。 - 析构函数必须在 .cpp 中定义(保证
Impl是完整类型)。 - 默认禁用拷贝,支持移动;如需拷贝则实现深拷贝。
3、简单示例
文件结构:
- Foo.h(对外接口)
- Foo.cpp(内部实现)
Foo.h
cpp
#pragma once
#include <memory>
class Foo {
public:
Foo();
~Foo(); // 在 .cpp 中定义
Foo(Foo&&) noexcept; // 支持移动
Foo& operator=(Foo&&) noexcept;
Foo(const Foo&) = delete; // 简化:不支持拷贝
Foo& operator=(const Foo&) = delete;
// 对外接口
void doWork();
int value() const;
private:
struct Impl; // 前向声明
std::unique_ptr<Impl> pimpl_; // 指向实现
};
Foo.cpp
cpp
#include "Foo.h"
// 仅在实现文件中引入重依赖
#include <vector>
#include <string>
#include <iostream>
struct Foo::Impl {
std::vector<int> data;
std::string name;
Impl() : data{1,2,3}, name("example") {}
void doWorkImpl() {
data.push_back(static_cast<int>(data.size()));
std::cout << "Working in Impl, name=" << name
<< ", size=" << data.size() << "\n";
}
int valueImpl() const {
return data.empty() ? 0 : data.back();
}
};
Foo::Foo()
: pimpl_(std::make_unique<Foo::Impl>()) {}
Foo::~Foo() = default;
Foo::Foo(Foo&&) noexcept = default;
Foo& Foo::operator=(Foo&&) noexcept = default;
void Foo::doWork() {
pimpl_->doWorkImpl();
}
int Foo::value() const {
return pimpl_->valueImpl();
}
使用
cpp
#include "Foo.h"
int main() {
Foo f;
f.doWork();
int v = f.value();
return v;
}
可选:支持拷贝(深拷贝)
如果需要拷贝语义,可为 Impl 提供克隆能力,然后在 Foo 中使用它。
cpp
// 在 Impl 中添加:
std::unique_ptr<Impl> clone() const {
auto c = std::make_unique<Impl>();
c->data = data;
c->name = name;
return c;
}
// 在 Foo.h 允许拷贝:
Foo(const Foo&);
Foo& operator=(const Foo&);
// 在 Foo.cpp 实现:
Foo::Foo(const Foo& other)
: pimpl_(other.pimpl_->clone()) {}
Foo& Foo::operator=(const Foo& other) {
if (this != &other) {
pimpl_ = other.pimpl_->clone();
}
return *this;
}
4、注意事项
- 将复杂或第三方依赖只放在 .cpp 中,头文件保持最小化。
- 析构函数在 .cpp 中定义,保证删除器看到完整类型。
- Pimpl 会带来一点指针间接访问开销,但通常利大于弊。
- 与桥接模式不同:Pimpl主要是编译与封装层面的技巧,桥接模式是面向设计的抽象与实现分离。