当前位置: 首页 > news >正文

当 使用 Pimpl 方式 时,在 实现文件 中定义特殊成员函数

先看一段代码:

// widget.hpp #pragma once #include <memory> class Widget { public: Widget(); Widget::~Widget() = default; Widget(Widget&& rhs) noexcept; Widget& operator=(Widget&& rhs) noexcept; Widget(const Widget& rhs) = delete; Widget& operator=(const Widget& rhs) = delete; void interface(); private: struct Impl; std::unique_ptr<Impl> pImpl; };

// widget.cpp

#include "widget.hpp" #include <iostream> #include <memory> #include <string> #include <utility> struct Widget::Impl { std::string value{"UserClassObject"}; void concreteInterface() { std::cout << value << std::endl; } }; Widget::Widget() : pImpl(std::make_unique<Impl>()) { } Widget::Widget(Widget&& rhs) noexcept = default; Widget& Widget::operator=(Widget&& rhs) noexcept = default; void Widget::interface() { pImpl->concreteInterface(); }

// client.cpp

#include "widget.hpp" #include <utility> int main() { Widget w1; Widget w2 = std::move(w1); Widget w3; w3 = std::move(w2); w3.interface(); return 0; }

编译:g++ -std=c++17 -Wall -Wextra -pedantic widget.cpp client.cpp -o widget_demo

编译错误信息如下:

In file included from client.cpp:5878:
widget.hpp:10:5: error: extra qualification 'Widget::' on member 'Widget' [-fpermissive]
10 | Widget::~Widget() = default;
| ^~~~~~
In file included from C:/mingw64/lib/gcc/x86_64-w64-mingw32/15.2.0/include/c++/memory:80,
from widget.hpp:3:
C:/mingw64/lib/gcc/x86_64-w64-mingw32/15.2.0/include/c++/bits/unique_ptr.h: In instantiation of 'void std::default_delete<_Tp>::operator()(_Tp*) const [with _Tp = Widget::Impl]':
C:/mingw64/lib/gcc/x86_64-w64-mingw32/15.2.0/include/c++/bits/unique_ptr.h:399:17: required from 'std::unique_ptr<_Tp, _Dp>::~unique_ptr() [with _Tp = Widget::Impl; _Dp = std::default_delete<Widget::Impl>]'
399 | get_deleter()(std::move(__ptr));
| ~~~~~~~~~~~~~^~~~~~~~~~~~~~~~~~
widget.hpp:10:5: required from here
10 | Widget::~Widget() = default;
| ^~~~~~
C:/mingw64/lib/gcc/x86_64-w64-mingw32/15.2.0/include/c++/bits/unique_ptr.h:91:23: error: invalid application of 'sizeof' to incomplete type 'Widget::Impl'
91 | static_assert(sizeof(_Tp)>0,
| ^~~~~~~~~~~

是什么原因造成的?

  1. 对于析构函数 ~Widget(); 这问题是由于生成w1析构时的目标代码报错。在此时,析构函数被调用,而在定义使用std::unique_ptr的Widget类中 ,我们并没有声明析构函数,因为我们不需要在Widget的析构函数内写任何代码。依据编译器自动生成特殊成员函数(参考条款17)的普通规则,编译器为我们生成了一个析构函数。在那个自动生成的析构函数中,编译器插入代码,调用Widget的数据成员pImpl的析构函数。pImpl是一个 std::unique_ptr<Widget::Impl> ,即,一个使用默认deleter的std::unique_ptr。默认deleter是一个函数,对std::unique_ptr里面的原生指针调用delete。然而,在调用delete之前,编译器通常会让默认deleter先使用C++11的static_assert来确保原生指针指向的类型不是imcomplete type(staticassert编译时候检查,assert运行时检查---译者注)。当编译器生成Widget w的析构函数时,调用的static_assert检查就会失败,导致出现了错误信息。在w被销毁时,这些错误信息才会出现,因为与编译器生成的其他特殊成员函数一样,Widget的析构函数也是inline的。出错指向w被创建的那一行,因为该行创建了w1,导致后来(w1出作用域时)w被隐性销毁。为了修复这个问题,你需要确保,在生成销毁 std::unique_ptr<Widget::Impl> 的代码时,Widget::Impl是完整的类型。当它的定义被编译器看到时,它就是完整类型了。而Widget::Impl在widget.cpp中被定义。所以编译成功的关键在于,让编译器只在widget.cpp内,在widget::Impl被定义之后,看到Widget的析构函数体(该函数体就是放置编译器自动生成销毁std::unique_ptr数据成员的代码的地方)。

在widget.h中声明Widget的析构函数,但不要定义,即:析构函数不要在头文件里 inline 默认;应放到.cpp中,让编译器在有机会看到完整Impl的地方生成它。

2. 对于移动赋值运算符:对于编译器生成的move赋值运算符,它对pImpl再赋值之前,需要先销毁它所指向的对象,然而在Widget头文件中,pImpl指向的仍是一个incomplete type; w1 = std::move(w2); w1需要先销毁;

3. 对于移动构造函数:它的问题在于编译器通常是在move构造函数内抛出异常的事件中,生成析构pImpl的代码,而对pImpl析构需要Impl的类型是完整的。

问题一样,解决方法自然也一样:将move操作的定义写在实现文件widget.cpp中;

如果我们用std::shared_ptr替代std::unique_ptr,我们会发现本条款的建议不再适用了。std::unique_ptr和std::shared_ptr的表现行为不同的原因是:它们之间支持自定义deleter的方式不同。对于std::unique_ptr,deleter的类型是智能指针的一部分,这就使得编译器能够生成更小的运行时数据结构以及更快的运行时代码。更高效率的结果是要求当编译器生成特殊函数时,std::unique_ptr所指向的类型必须是完整的。对于std::shared_ptr来说,deleter的类型不是智能指针的一部分。虽然会造成大一点的运行时数据结构和稍微慢一些的代码。但是在编译器生成特殊函数时,指向的类型不需要是完整的。

我们修改代码如下:

widget.hpp

// widget.hpp #pragma once #include <memory> class Widget { public: Widget(); ~Widget(); Widget(Widget&& rhs) noexcept; Widget& operator=(Widget&& rhs) noexcept; Widget(const Widget& rhs) = delete; Widget& operator=(const Widget& rhs) = delete; void interface(); private: struct Impl; std::unique_ptr<Impl> pImpl; };

widget.cpp

#include "widget.hpp" #include <iostream> #include <memory> #include <string> #include <utility> struct Widget::Impl { std::string value{"UserClassObject"}; void concreteInterface() { std::cout << value << std::endl; } }; Widget::Widget() : pImpl(std::make_unique<Impl>()) { } Widget::~Widget() = default; Widget::Widget(Widget&& rhs) noexcept = default; Widget& Widget::operator=(Widget&& rhs) noexcept = default; void Widget::interface() { pImpl->concreteInterface(); }

client.cpp

#include "widget.hpp" #include <utility> int main() { Widget w1; Widget w2 = std::move(w1); Widget w3; w3 = std::move(w2); w3.interface(); return 0; }

编译通过:g++ -std=c++17 -Wall -Wextra -pedantic widget.cpp client.cpp -o widget_demo

如果我们在cpp文件中,把 ~Widget放到Impl的前面:

#include "widget.hpp" #include <iostream> #include <memory> #include <string> #include <utility> Widget::~Widget() = default; // cpp 中的析构函数放到 Impl定义的前面也是OK的 struct Widget::Impl { std::string value{"UserClassObject"}; void concreteInterface() { std::cout << value << std::endl; } }; Widget::Widget() : pImpl(std::make_unique<Impl>()) { } Widget::Widget(Widget&& rhs) noexcept = default; Widget& Widget::operator=(Widget&& rhs) noexcept = default; void Widget::interface() { pImpl->concreteInterface(); }

编译也是通过的:

虽然写在Impl定义前面,但它已经是在.cpp文件里做“类外定义” 了,不再是头文件里的 inline 默认析构,所以很多编译器/标准库实现下仍然可以通过。

核心原因是这两个“时机”不是一回事:

  1. 你写出~Widget() = default;的位置
  2. 编译器真正去实例化std::unique_ptr<Impl>析构逻辑的时机

Widget的析构本质上会去析构成员:

std::unique_ptr<Impl> pImpl;

unique_ptr<Impl>真正触发delete Impl*的检查,往往是在它的析构函数模板被实例化的时候。这个实例化很多实现会延后到编译器已经看完整个.cpp,那时下面这个定义已经出现了:

struct Widget::Impl

{

...

};

所以它就能通过。

为什么头文件里= default常常不行?

如果你写成头文件里这样:

class Widget

{

public:

~Widget() = default;

...

};

那这个析构函数就是 inline 的,任何#include "widget.hpp"的翻译单元都可能当场去生成它。而这些地方看不到Impl的完整定义,于是unique_ptr<Impl>的析构就会因为Impl是不完整类型而报错。

也就是说,真正危险的是:

  • .hpp里默认析构
  • 而不是
  • .cpp里类外= default,但写在Impl前面

但为什么我仍然建议把它放在Impl后面?

因为这样更清晰,也更稳妥:

struct Widget::Impl

{

...

};

Widget::~Widget() = default;

好处是:

  • 代码语义更直观:先看到完整Impl,再定义会销毁它的析构
  • 避免读代码的人误解
  • 某些实现细节差异下更不容易踩坑
  • 面试/代码评审里这是更标准的写法

如果我们把move constuctor的default放到hpp文件中

// widget.hpp

#pragma once #include <memory> class Widget { public: Widget(); ~Widget(); // Widget(Widget&& rhs) noexcept; Widget::Widget(Widget&& rhs) noexcept = default; //移动构造default在hpp文件中 Widget& operator=(Widget&& rhs) noexcept; Widget(const Widget& rhs) = delete; Widget& operator=(const Widget& rhs) = delete; void interface(); private: struct Impl; std::unique_ptr<Impl> pImpl; };

// widget.cpp

#include "widget.hpp" #include <iostream> #include <memory> #include <string> #include <utility> // Widget::~Widget() = default; struct Widget::Impl { std::string value{"UserClassObject"}; void concreteInterface() { std::cout << value << std::endl; } }; Widget::Widget() : pImpl(std::make_unique<Impl>()) { } Widget::~Widget() = default; //从cpp文件中删除了 移动构造函数 // Widget::Widget(Widget&& rhs) noexcept = default; Widget& Widget::operator=(Widget&& rhs) noexcept = default; void Widget::interface() { pImpl->concreteInterface(); }

编译错误:

In file included from client.cpp:5878:
widget.hpp:12:5: error: extra qualification 'Widget::' on member 'Widget' [-fpermissive]
12 |Widget::Widget(Widget&& rhs) noexcept = default;
| ^~~~~~
In file included from C:/mingw64/lib/gcc/x86_64-w64-mingw32/15.2.0/include/c++/memory:80,
from widget.hpp:3:
C:/mingw64/lib/gcc/x86_64-w64-mingw32/15.2.0/include/c++/bits/unique_ptr.h: In instantiation of 'void std::default_delete<_Tp>::operator()(_Tp*) const [with _Tp = Widget::Impl]':
C:/mingw64/lib/gcc/x86_64-w64-mingw32/15.2.0/include/c++/bits/unique_ptr.h:399:17: required from 'std::unique_ptr<_Tp, _Dp>::~unique_ptr() [with _Tp = Widget::Impl;_Dp = std::default_delete<Widget::Impl>]'
399 | get_deleter()(std::move(__ptr));

| ~~~~~~~~~~~~~^~~~~~~~~~~~~~~~~~
widget.hpp:12:5: required from here
12 | Widget::Widget(Widget&& rhs) noexcept = default;
| ^~~~~~
C:/mingw64/lib/gcc/x86_64-w64-mingw32/15.2.0/include/c++/bits/unique_ptr.h:91:23: error: invalid application of 'sizeof' toincomplete type 'Widget::Impl'
91 |static_assert(sizeof(_Tp)>0,
| ^~~~~~~~~~~

http://www.jsqmd.com/news/712183/

相关文章:

  • 新版Docker AI Toolkit到底值不值得升?深度对比2025→2026性能跃迁数据,92%团队已在48小时内完成迁移
  • 题解:洛谷 B2140 二进制分类
  • 电磁车电感布局实战:水平、八字、T型,哪种方案过弯更稳?附LMV358电路实测数据
  • hyperf 数据生命周期管理
  • MusicDownload:你的个人音乐库自由之路,三步开启免费音乐收藏新体验
  • std::shared_ptr的别名构造函数
  • PLCopen规范C语言移植项目交付倒计时!——紧急上线前必须验证的7项合规性测试(含TUV认证预检Checklist)
  • RTL设计和HLS高层次设计
  • C++实现计算器功能
  • LACIN网络架构:完全互连网络的创新实现与优化
  • X平台算法解析:掌握黄金法则提升内容触及率与互动率
  • SAP ABAP实战:用BAPI ME_INFORECORD_MAINTAIN批量维护采购信息记录(含价格等级完整代码)
  • 收藏!全国首所网安本科高校2026招生!小白_程序员入行必看
  • 2026年水玻璃厂家专业度判定指南:水玻璃厂家哪家口碑好/水玻璃厂家哪家大/水玻璃厂家哪家实力大/水玻璃厂家哪家实力强/选择指南 - 优质品牌商家
  • Kafka集群管理新选择:深度体验Kafka-UI,对比CMAK/Offset Explorer谁更香?
  • DynamicVerse框架:4D动态场景重建与语义理解技术解析
  • 生产系统里维护 SAP Gateway System Alias 的正确打开方式
  • Flux Tasks API 的集成与使用指南
  • 参数传递规则问题-类型匹配
  • Smol轻量级模型:高效神经网络架构设计与应用
  • bool值不等于0都是true
  • 链表中倒数第k个结点-C++
  • 别再为CWRU轴承数据发愁了!一个Python函数搞定数据读取与划分(附完整代码)
  • ARM GICv3虚拟中断控制器与ICV_HPPIR1_EL1寄存器详解
  • 多项式优化框架设计与Julia实现实践
  • 解锁macOS视频预览新境界:QuickLookVideo全面解析与实战指南
  • Leetcode刷题总结-3.二叉树篇
  • 实战:在华为Atlas 300i Pro上部署YOLOv5模型进行目标检测(MindSpore+CANN)
  • 终极Django REST Framework数据分析指南:API使用统计与业务洞察实战
  • RPG Maker Decrypter技术深度解析:三版本加密算法实现与架构设计