ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

Pimpl 模式(d-pointer)详解:如何解决 C++ 头文件过大、编译依赖和 ABI 兼容问题

Pimpl 模式(d-pointer)详解:如何解决 C++ 头文件过大、编译依赖和 ABI 兼容问题 文章目录Pimpl 模式d-pointer详解如何解决 C 头文件过大、编译依赖和 ABI 兼容问题1. 普通 C 类的问题Camera.hCamera.cpp2. C 为什么头文件大会导致问题3. Pimpl 的核心思想4. 最简单 Pimpl 实现Camera.h5. 实现放入 cppCamera.cpp6. Pimpl 如何解决头文件过大的问题普通方式Pimpl方式7. Pimpl 的第二个作用隐藏 ABIPimpl8. Qt 的 d-pointer 实现方式9. Pimpl 与普通封装有什么区别10. C11 推荐写法11. 为什么析构函数必须放 cpp12. Pimpl 的缺点缺点1一次额外间接访问缺点2增加动态内存缺点3代码复杂度增加13. 什么场景适合 Pimpl1. 开发大型库2. 大型 C 工程3. 第三方 SDK14. Pimpl 实际工程结构15. 总结1. 减少头文件依赖2. 加快编译速度3. 保持 ABI 稳定Pimpl 模式d-pointer详解如何解决 C 头文件过大、编译依赖和 ABI 兼容问题在大型 C 项目中经常会遇到这样的问题一个简单的类classCamera{public:voidopen();voidclose();private:cv::Mat image_;std::vectorDevicedevices_;std::mutex mutex_;NetworkClient client_;AlgorithmEngine engine_;};看起来只是一个Camera类但是它的头文件可能需要#includeopencv2/opencv.hpp#includevector#includemutex#includeDevice.h#includeNetworkClient.h#includeAlgorithmEngine.h结果Camera.h变得巨大每个包含Camera.h的文件都会间接包含大量头文件修改一个私有成员可能导致整个工程重新编译动态库升级容易破坏 ABI 兼容为了解决这些问题C 中出现了一种经典设计PimplPointer to implementation模式也叫 d-pointer、opaque pointer、Compiler Firewall。它通过一个指针把类的实现细节隐藏到.cpp文件中。(cppreference)Qt 大量使用这种设计例如 Qt 源码中常见Q_D(QWidget);Q_Q(QWidget);这些宏就是 d-pointer 模式的一部分。(Qt Wiki)1. 普通 C 类的问题假设我们有Camera.h#pragmaonce#includeopencv2/opencv.hpp#includevector#includemutex#includeDevice.h#includeNetworkClient.h#includeAlgorithmEngine.hclassCamera{public:Camera();voidopen();private:cv::Mat image_;std::vectorDevicedevices_;std::mutex mutex_;NetworkClient client_;AlgorithmEngine engine_;};Camera.cpp#includeCamera.hCamera::Camera(){}voidCamera::open(){}表面上Camera||--open()实际上Camera.h | |-- OpenCV | |-- Device | |-- NetworkClient | |-- AlgorithmEngine | |-- mutex | |-- vector形成巨大依赖树。2. C 为什么头文件大会导致问题C 的编译模型main.cpp #include Camera.h | | v 展开 Camera.h | | v 编译整个文件如果100 个 cppA.cpp B.cpp C.cpp ...都#includeCamera.h那么Camera.h修改 | | v 100个cpp全部重新编译例如今天增加一个成员private:AlgorithmEngine engine_;改成private:AlgorithmEngine engine_;NewAIModel model_;虽然只是内部实现变化。但是Camera.h改变 ↓ 所有include Camera.h的文件重新编译大型工程5000 cpp 重新编译 30分钟这就是 C 大项目常见的编译瓶颈。3. Pimpl 的核心思想PimplPointer to implementation意思不直接保存实现而保存一个指向实现的指针。结构变成Camera ---------------- | public API | | | | Impl* d | ---------------- | | v ---------------- | private data | | | | OpenCV | | Algorithm | | Network | ----------------4. 最简单 Pimpl 实现Camera.h变成#pragmaonce#includememoryclassCameraPrivate;classCamera{public:Camera();~Camera();voidopen();private:std::unique_ptrCameraPrivated;};注意这里没有#includeopencv2/opencv.hpp#includeAlgorithmEngine.h#includeNetworkClient.h只有classCameraPrivate;这叫前向声明forward declaration5. 实现放入 cppCamera.cpp#includeCamera.h#includeopencv2/opencv.hpp#includeAlgorithmEngine.h#includeNetworkClient.hclassCameraPrivate{public:cv::Mat image_;AlgorithmEngine engine_;NetworkClient client_;};Camera::Camera():d(newCameraPrivate){}Camera::~Camera()default;voidCamera::open(){d-engine_.start();}现在Camera.h 只知道 CameraPrivate*但是Camera.cpp 知道 CameraPrivate全部内容6. Pimpl 如何解决头文件过大的问题对比普通方式Camera.h #include OpenCV #include Network #include Algorithm class Camera { cv::Mat image; NetworkClient client; AlgorithmEngine engine; };依赖Camera.h | -- OpenCV | -- Network | -- Algorithm | -- STLPimpl方式Camera.h class CameraPrivate; class Camera { CameraPrivate* d; };依赖Camera.h | | -- CameraPrivate声明真正依赖Camera.cpp | -- OpenCV | -- Network | -- Algorithm结果头文件从几十KB 甚至几百KB降低到几KB7. Pimpl 的第二个作用隐藏 ABI这是 Qt 最看重的地方。假设发布一个动态库libcamera.dll用户代码Camera cam;编译时编译器需要知道sizeof(Camera)例如Camera: image_ 64 bytes client_ 128 bytes engine_ 256 bytes sizeof(Camera)448如果升级增加NewAIModel model_;变成sizeof(Camera)1024旧程序不知道。产生 ABI 崩溃。Pimpl用户看到classCamera{CameraPrivate*d;};大小sizeof(Camera) sizeof(pointer) 8 bytes以后内部变化版本1CameraPrivate image client版本2CameraPrivate image client AIModel GPUContext Cache但是外部Camera 永远 8 bytes所以ABI 不变。这也是 Qt 能长期保持二进制兼容的重要原因。(Qt Wiki)8. Qt 的 d-pointer 实现方式Qt 类例如classQWidget{public:voidshow();private:QWidgetPrivate*d_ptr;};内部classQWidgetPrivate{public:QWidget*q_ptr;QRect geometry;QWidgetData data;};关系QWidget | | v QWidgetPrivate | | v QWidget所以 Qt 中Q_D(QWidget);实际上QWidgetPrivate*dd_func();9. Pimpl 与普通封装有什么区别很多人认为private 成员已经隐藏了。例如classPerson{private:std::string name_;};为什么还需要 Pimpl原因private只能防止用户访问不能防止编译依赖 ABI变化比较privatePimpl隐藏数据√√减少头文件依赖×√减少编译时间×√ABI稳定×√增加间接访问×√10. C11 推荐写法现代 C推荐classWidget{public:Widget();~Widget();private:classImpl;std::unique_ptrImplimpl_;};cppclassWidget::Impl{public:std::string name;intvalue;};Widget::Widget():impl_(std::make_uniqueImpl()){}11. 为什么析构函数必须放 cpp错误classWidget{private:std::unique_ptrImplimpl_;};可能出现invalid application of sizeof incomplete type原因unique_ptr析构时需要知道sizeof(Impl)解决头文件classWidget{public:~Widget();private:classImpl;std::unique_ptrImplimpl_;};cppWidget::~Widget()default;12. Pimpl 的缺点不是所有类都应该使用。缺点1一次额外间接访问普通object.member 一次访问Pimplobject | pointer | member多一次指针跳转。缺点2增加动态内存通常newImpl导致heap allocation缺点3代码复杂度增加简单类classPoint{intx;inty;};没有必要PointPrivate13. 什么场景适合 Pimpl1. 开发大型库例如Qt OpenCV模块 商业SDK 插件系统需要ABI稳定隐藏实现2. 大型 C 工程例如你的工程ProtocolManager DeviceManager AlgorithmEngine Renderer Controller如果头文件#includeFastDDS OpenCV VTK CUDA Boost建议Pimpl。3. 第三方 SDK例如你的接口classRobotSDK{};内部DDS TCP Camera GPU 算法用户只需要#includeRobotSDK.h14. Pimpl 实际工程结构推荐Camera/ Camera.h Camera.cpp CameraPrivate.hCamera.h:classCameraPrivate;classCamera{CameraPrivate*d;};CameraPrivate.h:classCameraPrivate{cv::Mat image;vtkRenderer renderer;};15. 总结Pimpld-pointer的核心用一个稳定的小接口类持有一个指向真实实现的指针把复杂实现移动到 cpp。它解决三个核心问题1. 减少头文件依赖以前Camera.h | OpenCV VTK DDS Boost之后Camera.h | CameraPrivate*2. 加快编译速度修改CameraPrivate只重新编译Camera.cpp而不是整个工程3. 保持 ABI 稳定对象布局以前Camera大小随成员变化之后Camera 一个指针大小内部可以无限扩展因此Qt大型 C SDK跨版本动态库工业软件大量采用 Pimpl / d-pointer 设计。(Qt Wiki)对于 Qt CMake MSVC 大型视觉/工业软件项目Pimpl 是非常值得掌握的架构级 C 技术。
返回列表