C++ 设计模式:Pimpl(Pointer to Implementation)

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主要是编译与封装层面的技巧,桥接模式是面向设计的抽象与实现分离。
相关推荐
秋邱3 小时前
用 Python 写出 C++ 的性能?用CANN中PyPTO 算子开发硬核上手指南
开发语言·c++·python
我在人间贩卖青春3 小时前
C++之析构函数
c++·析构函数
野犬寒鸦3 小时前
从零起步学习并发编程 || 第六章:ReentrantLock与synchronized 的辨析及运用
java·服务器·数据库·后端·学习·算法
霖霖总总3 小时前
[小技巧66]当自增主键耗尽:MySQL 主键溢出问题深度解析与雪花算法替代方案
mysql·算法
rainbow68893 小时前
深入解析C++STL:map与set底层奥秘
java·数据结构·算法
我在人间贩卖青春3 小时前
C++之数据类型的扩展
c++·字符串·数据类型
wangjialelele4 小时前
平衡二叉搜索树:AVL树和红黑树
java·c语言·开发语言·数据结构·c++·算法·深度优先
苏宸啊4 小时前
C++栈和队列
c++
森G4 小时前
七、04ledc-sdk--------makefile有变化
linux·c语言·arm开发·c++·ubuntu
驱动探索者4 小时前
linux mailbox 学习
linux·学习·算法