复制构造函数必须深拷贝pimpl指针,否则浅拷贝导致双重释放;正确做法是用make_unique(*other.pimpl_);若impl不可拷贝则需删除拷贝构造函数。

复制构造函数必须深拷贝 pimpl 指针
如果你让 Pimpl 类默认使用合成的复制构造函数,它只会浅拷贝原始指针——两个对象最终指向同一块堆内存,析构时触发双重释放(double free)或未定义行为。这是最常踩的坑。
正确做法是显式定义复制构造函数,并在其中分配新内存、调用 Pimpl 类型的拷贝构造函数:
class Widget {
struct Impl;
std::unique_ptr<impl> pimpl_;
public:
Widget(const Widget& other) : pimpl_(std::make_unique<impl>(*other.pimpl_)) {}
};</impl></impl>
- 不能直接写
pimpl_(other.pimpl_.get())—— 这只是指针赋值,没做深拷贝 - 如果
Impl不可拷贝(比如含std::mutex或std::thread),那整个Widget就不该支持复制,应删除拷贝构造函数:Widget(const Widget&) = delete; - 注意
std::make_unique<impl>(*other.pimpl_) </impl>要求Impl有可访问的拷贝构造函数
operator= 必须处理自赋值并释放旧资源
赋值运算符比复制构造函数更复杂:要先清理当前 pimpl_ 所指内存,再深拷贝;同时必须能安全应对 w = w; 这种自赋值场景。
推荐用「复制-交换」惯用法(copy-and-swap),既简洁又天然避免自赋值问题:
Widget& Widget::operator=(const Widget& other) {
if (this != &other) {
*pimpl_ = *other.pimpl_; // 前提:Impl 支持赋值
}
return *this;
}
- 上面这种写法依赖
Impl的operator=,适合Impl成员都支持赋值的场景 - 更通用的做法是先
pimpl_ = std::make_unique<impl>(*other.pimpl_)</impl>,但要注意异常安全:若new抛异常,原对象状态已被破坏 - 用
std::swap(pimpl_, temp.pimpl_)配合临时对象,才能保证强异常安全,不过会多一次移动构造开销
移动语义可以默认生成,但要注意 Impl 是否可移动
只要 Impl 类型本身支持移动(即没有显式删除移动操作,且所有成员可移动),std::unique_ptr<impl></impl> 就能自动转发移动语义,你无需写移动构造函数和移动赋值运算符。
- 如果
Impl含不可移动成员(如std::mutex),那么std::unique_ptr<impl></impl>仍可移动——因为unique_ptr移动只转移指针所有权,不调用Impl的移动构造函数 - 但此时
Widget的移动构造函数若被默认生成,会尝试移动pimpl_,这没问题;而移动赋值若用默认实现,也会正确转移指针 - 唯一例外:你显式声明了拷贝操作但没声明移动操作,编译器就不会生成移动函数,这时得手动加
= default
别忽略 const 成员函数对 pimpl 的访问限制
复制行为看似只关乎构造/赋值,但实际影响所有使用 pimpl 的地方。比如你在 const 成员函数里想读取 pimpl_ 数据,而 Impl 的对应函数没加 const 限定,就会编译失败。
- 复制逻辑本身不引发这个问题,但一旦你开始调试“为什么
getFoo()在 const 对象上调用不了”,根源往往在Impl的接口设计不一致 - 尤其当
Impl用了mutable缓存或惰性初始化时,const成员函数内部仍需修改pimpl_指向的对象,此时Impl的对应函数必须是const的 - 这个细节和复制无直接关系,但所有使用
pimpl的类都会撞上——而且容易在测试阶段才暴露
C++免费学习笔记(深入):立即使用
在学习笔记中,你将探索 C++ 的入门与实战技巧!











