- Java锁的逻辑(结合对象头和ObjectMonitor)
- 还在用饼状图?来瞧瞧这些炫酷的百分比可视化新图形(附代码实现)⛵
- 自动注册实体类到EntityFrameworkCore上下文,并适配ABP及ABPVNext
- 基于Sklearn机器学习代码实战
C++ 里面有一些惯用法(idioms),如 RAII,PIMPL,copy-swap、CRTP、SFINAE 等。今天要说的是 PIMPL,即 Pointer To Implementation,指向实现的指针.
在实际的项目中,经常需要定义和第三方/供应商的 C++ 接口。假如有这样一个接口文件:
MyInterface.h 。
#include <string>
#include <list>
#include "dds.h"
class MyInterface {
public:
int publicApi1();
int publicApi2();
private:
int privateMethod1();
int privateMethod2();
int privateMethod3();
private:
std::string name_;
std::list<int> list_;
DDSDomainPariciant dp_;
DDSTopic topic_;
DDSDataWriter dw_;
};
该接口头文件存在以下问题:
PIMPL 就是 C++ 里专门用来解决这些问题的惯用法。 PIMPL 将 MyInterface 类的具体实现(private/protected 方法、成员)转移到另外一个嵌套类 Impl 中,然后利用前向声明(forward declaration)声明 Impl,并在原有的 MyInterface 接口类中增加一个指向 Impl 对象的指针。 再次强调,在 MyInterface 中的 Impl 仅仅是一个前向声明,MyInterface 类只知道有 Impl 这么个类,但是对 Impl 有哪些方法、哪些成员变量一无所知,因此能做的事情非常有限(声明一个指向该类的指针就是其中之一)。而这恰恰就是 PIMPL 将接口和实现解耦的关键所在.
应用 PIMPL 后的 MyInterface.h 文件:
class MyInterface {
public:
MyInterface();
~MyInterface();
int publicApi1();
int publicApi2();
private:
struct Impl;
Impl* impl_;
};
现在 MyInterface.h 接口文件变得非常清爽。原本在 MyInterface.h 中的那些依赖、实现细节,现在通通转移到了 MyInterface.cpp 内部, 对接口的使用者彻底隐藏,降低使用者的依赖,提高接口稳定性 :
MyInterface.cpp 。
#include <string>
#include <list>
#include "dds.h"
struct MyInterface::Impl {
int publicApi1();
int publicApi2(int i);
int privateMethod1();
int privateMethod2();
int privateMethod3();
std::string name_;
std::list<int> list_;
DDSDomainPariciant dp_;
DDSTopic topic_;
DDSDataWriter dw_;
};
MyInterface::MyInterface()
: pimpl_(new Impl()) {}
MyInterface::~MyInterface() {
delete pimpl_;
}
int MyInterface::publicApi1() {
impl_->publicApi1();
}
int MyInterface::publicApi2(int i) {
impl_->publicApi2(i);
}
// 其他 MyInterface::Impl 类的方法实现
// 原本 MyInterface 中的逻辑挪到 MyInterface::Impl 中
int MyInterface::Impl::publicApi1() {...}
可以看到,MyInterface 类的实现本身只是单纯地将请求委托/转发给 MyInterface::Impl 的同名方法。对于参数的传递,也可以适当使用 std::move 提升效率(关于 std::move 今后也可以展开说说).
也可以把嵌套类 MyInterface::Impl 放到单独 MyInterfaceImpl.h/cpp 中,如此一来 MyInterface.cpp 就会变得非常简洁,就像下面这样:
MyInterface.cpp 。
#include "MyInterface.h"
#include "MyInterfaceImpl.h"
MyInterface::MyInterface()
: pimpl_(new Impl()) {}
MyInterface::~MyInterface() {
delete pimpl_;
}
int MyInterface::publicApi1() {
return impl_->publicApi1();
}
int MyInterface::publicApi2(int i) {
return impl_->publicApi2(i);
}
MyInterfaceImpl.h 。
#include <string>
#include <list>
#include "dds.h"
struct MyInterface::Impl {
int publicApi1();
int publicApi2(int i);
int privateMethod1();
int privateMethod2();
int privateMethod3();
std::string name_;
std::list<int> list_;
DDSDomainPariciant dp_;
DDSTopic topic_;
DDSDataWriter dw_;
};
MyInterfaceImpl.cpp 。
#include "MyInterfaceImpl.h"
int MyInterface::Impl::publicApi1() {
// ...
}
// 其他 MyInterface::Impl 类的方法实现
注意不要在 MyInterface.h 中 #include "MyInterfaceImpl.h",否则就前功尽弃了.
以上是传统 C++ 中的 PIMPL 的实现,现代 C++ 应尽量避免使用裸指针,而使用智能指针。具体的原因见这篇文章「 裸指针七宗罪 」.
MyInterface 拥有 Impl 对象的专属所有权,unique_ptr 是最自然的选择。如果直接将上述的裸指针替换成 unique_ptr:
#include <memory>
class MyInterface {
public:
MyInterface();
int publicApi1();
int publicApi2();
private:
struct Impl;
std::unique_ptr<Impl> impl_;
};
// main.cpp
int main() {
MyInterface if;
}
会看到这样的报错:
/opt/compiler-explorer/gcc-13.1.0/include/c++/13.1.0/bits/unique_ptr.h: In instantiation of 'constexpr void std::default_delete<_Tp>::operator()(_Tp*) const [with _Tp = MyInterface::Impl]':
/opt/compiler-explorer/gcc-13.1.0/include/c++/13.1.0/bits/unique_ptr.h:404:17: required from 'constexpr std::unique_ptr<_Tp, _Dp>::~unique_ptr() [with _Tp = MyInterface::Impl; _Dp = std::default_delete<MyInterface::Impl>]'
<source>:118:7: required from here
/opt/compiler-explorer/gcc-13.1.0/include/c++/13.1.0/bits/unique_ptr.h:97:23: error: invalid application of 'sizeof' to incomplete type 'MyInterface::Impl'
97 | static_assert(sizeof(_Tp)>0,
| ^~~~~~~~~~~
问题出在哪里呢?
问题就出在 MyInterface 的析构函数。在没有显式声明析构函数的情况下,编译器会自动合成一个隐式内联的析构函数(编译器在什么条件下,自动合成哪些函数也有不少学问,后面会单独发一篇),等效代码如下:
class MyInterface {
public:
MyInterface();
~MyInterface(){} // 是实现,不是声明!
int publicApi1();
int publicApi2();
private:
struct Impl;
std::unique_ptr<Impl> impl_;
};
在 MyInterface.h 中,编译器自动合成的析构函数会进行以下操作:
unique_ptr<Impl> impl_
成员 我们所看到报错,就出在第 5 步。unique_ptr 的实现代码在删除前,会进行 static_assert(sizeof(_Tp)>0 断言,而编译器执行该断言的时候,Impl 还是一个不完整类型(Incomplete Type)。因为编译器此时只看到了 MyInterface::Impl 的前向声明,还没有看到定义,不知道 Impl 有哪些成员,也不知 Impl 类占用多大内存,所以在进行 sizeof(Impl) 的时候报错.
知道了背后的原理,解决起来也很简单,就是保证在 MyInterface 析构函数实现的地方,能看到 Impl 类的定义即可:
MyInterface.h 。
#include <memory>
class MyInterface {
public:
MyInterface();
~MyInterface(); // 使用 unique_ptr 的关键:只声明,不实现!
int publicApi1();
int publicApi2();
private:
struct Impl;
std::unique_ptr<Impl> impl_;
};
MyInterface.cpp 。
#include <memory>
#include "MyInterface.h"
#include "MyInterfaceImpl.h"
MyInterface::MyInterface()
: pImpl_(std::make_unique<Impl>()) {}
MyInterface::~MyInterface() = default;
int MyInterface::publicApi1() {
return impl_->publicApi1();
}
int MyInterface::publicApi2(int i) {
return impl_->publicApi2(i);
}
这样,一个正确的 PIMPL 就搞定啦!虽然 PIMPL 多了一层封装,稍微增加了一点点复杂度,但我认为这么做是绝对的利大于弊。以一个我曾参与的项目为例,在将近一年的时间里,实现库更新了很多版,但是接口文件从释放以来一直没变过,大大减少了和第三方/供应商的沟通、调试成本.
最后,留一个思考题:为什么将 unique_ptr 换成 shared_ptr 不会遇到上面的 static_assert(sizeof(_Tp)>0 编译错误?如果你能解释其中的原因,那说明你对 shared_ptr、unique_ptr 的理解相当深入了👏 。
原文地址: https://www.cnblogs.com/tengzijian/p/17473602.html 。
最后此篇关于设计C++接口文件的小技巧之PIMPL的文章就讲到这里了,如果你想了解更多关于设计C++接口文件的小技巧之PIMPL的内容请搜索CFSDN的文章或继续浏览相关文章,希望大家以后支持我的博客! 。
我有两个实现 PIMPL 的类:State 和 StateMachine。 它们实现了 Private::State 和 Private::StateMachine。 Private::StateMa
考虑以下两种实现 pimpl 习语的方法: // file g_visible.h //global forward declarations class HiddenStuff_A; class
我目前正在研究pimpl idiom,并且有非常好的教程介绍如何实现它(例如 here )。但我从未见过它像这样作为基本模板类实现: #ifndef PIMPL_H #define PIMPL_H t
这个问题在这里已经有了答案: Move semantics == custom swap function obsolete? (5 个答案) 关闭 2 年前。 我有几个基于 PIMPL 习语的类(
我正在开发一个小型 IO 库,其中接口(interface)的要求是已知的,但实现可能会发生变化。该库应该以存档格式读取和写入文件,并存储一些元数据。 我考虑过使用 pimpl,因为它似乎非常适合这项
我正在尝试为 pimpl idiom 创建一个实用程序类,但是我遇到了一些问题,希望得到一些帮助: 这就是我得到的: [sehe:另请参阅此处的 rev.1:https://gist.github.c
请看下面的代码(一代码胜一千字): 形状.hpp class Shape { public: double area() const; private: class ShapeImpl
我想更好地理解如何在 PIMPL 习语存在的情况下使用静态字段方法。考虑以下代码。 MyClass.h 文件: #ifndef MYCLASS #define MYCLASS class MyClas
假设我有一个 B 类型的对象,并调用 B.foo(),其中 foo() 是定义的方法在 A 中并且尚未在 B 中重新定义。 A::foo() 有 impl->foo() 行。 当我们调用 B.foo(
我正在从 http://en.wikibooks.org/wiki/C%2B%2B_Programming/Code/Design_Patterns#Creational_Patterns 中阅读有关
我正在使用 pimpl 惯用法实现几个类,并且遇到了一些设计问题。 首先,我一直看到pimpl是这样做的 class Object { public: Visible(); ~Visi
.h public: void doStuff() const; private: struct Private; Private * d; .cpp struct XX::P
所以我一直在思考PIMPL和堆栈分配。我一直在编写一个库,并决定使用 PIMPL 来隐藏该类的私有(private)成员。这意味着我将有一个这样声明的类 class Foo { private:
我正在尝试使用 pimpl 模式并在匿名命名空间中定义实现类。这在 C++ 中可能吗?我失败的尝试如下所述。 是否可以在不将实现移动到具有名称(或全局名称)的 namespace 的情况下解决此问题?
考虑以下几点: PImpl.hpp class Impl; class PImpl { Impl* pimpl; PImpl() : pimpl(new Impl) { } ~
可以使用什么样的技巧来最小化实现 pImpl 类的工作量? 标题: class Foo { struct Impl; boost::scoped_ptr self; public:
请参阅我的 PIMPL 继承实现。在派生类中,DerivedImpl继承自BaseImpl。 问题:指向 Impl 的指针是否应该像下面的代码一样只在基类中定义?如果是这样,每次我需要使用基指针时,我
这是一个非常愚蠢的错误,但我不知道这里发生了什么。 有很多 pimpl 示例,但我不明白为什么这不起作用(这或多或少是示例之一,但我看不出有什么区别)。 我有一个非常简单的 Pimpl 示例,但它不起
在我的新工作场所,代码大量使用 Pimpl 惯用语,原因是为了减少编译时间。但是我有一个基本的查询——pimpl 不需要动态分配内存吗?因此,实际上我们在堆中分配了比需要更多的内存。如果它被大量使用,
考虑下一个简单的例子: 标题: // a.hpp #ifndef A_HPP #define A_HPP #include class A { public: A(); int foo()
我是一名优秀的程序员,十分优秀!