Impl模式早就有过接触(本文特指经过指针完成impl),我晓得它具备如下优势:c++
Impl会带来性能的损耗,每次访问都由于指针增长了间接性,还有一个微小的指针内存消耗。可是基于以上优势,除非你十分肯定它形成了性能损耗,不然就让它存在吧。程序员
Qt中大量使用Impl,具体可见https://wiki.qt.io/D-Pointer中关于Q_D和Q_Q宏的解释。微信
然而,如何使用智能指针,我是说基于std::unique_ptr实现正确的impl模式,就有点意思了。函数
#include <boost/noncopyable.hpp> #include <memory> class Trace1 : public boost::noncopyable { public: Trace1(); ~Trace1() = default; void test(); private: class TraceImpl; std::unique_ptr<TraceImpl> _impl; };
这是我第一版代码,关于_impl的实现细节,存放于cpp中,以下所示:性能
class Trace1::TraceImpl { public: TraceImpl() = default; static std::string test() { return "hello trace1"; } }; Trace1::Trace1() : _impl(std::make_unique<Trace1::TraceImpl>()) { } void Trace1::test() { std::cout << _impl->test() << std::endl; }
很无情,我遇到了错误,错误以下所示:ui
为何会这样呢,报错信息提示TraceImpl是一个不完整的类型。指针
其实,就是编译器看到TraceImpl,没法在编译期间肯定TraceImpl的大小。此处咱们使用的是std::unique_ptr,其中存放的是一个指针,不必知道TraceImpl的具体大小(换成std::shared_ptr就不会这个报错)。code
往上看报错信息,发现std::unique_ptr的析构函数有点意思:blog
/usr/include/c++/7/bits/unique_ptr.h: In instantiation of ‘void std::default_delete<_Tp>::operator()(_Tp*) const [with _Tp = Trace1::TraceImpl]’: /usr/include/c++/7/bits/unique_ptr.h:268:17: required from ‘std::unique_ptr<_Tp, _Dp>::~unique_ptr() [with _Tp = Trace1::TraceImpl; _Dp = std::default_delete<Trace1::TraceImpl>]’ /home/jinxd/CLionProjects/impltest/include/Trace1.h:16:5: required from ‘void std::default_delete<_Tp>::operator()(_Tp*) const [with _Tp = Trace1]’ /usr/include/c++/7/bits/unique_ptr.h:268:17: required from ‘std::unique_ptr<_Tp, _Dp>::~unique_ptr() [with _Tp = Trace1; _Dp = std::default_delete<Trace1>]’
报错信息中,有两段提到了析构函数,并且都是默认析构函数:std::default_delete<_Tp>。应该知道,咱们的代码在编译的时候,会被编译器往里面添加点做料。按照c++的哲学就是,你不须要知道咱们添加了什么,你只须要晓得添加后的结果是什么。但是,为了解决错误,咱们必须知道大概添加了什么。内存
代码中,Trace1的析构函数标记为default,函数体中无具体代码,Trace1的析构函数有很大的可能性被inline了。若是函数被inline了,那么引用Trace1.h的main文件中,析构函数会被文本段落展开。
之前我就就在想,析构函数中没有代码,展开也不该该产生影响。错就错在,编译以后的析构函数被扩展了,塞入了_impl的销毁代码。销毁_impl必然会调用到std::unique_ptr的析构函数。std:unique_ptr在销毁的时候,会调用构造函数中传来的析构函数(若是你没有显式提供析构函数,那么就是用编译器扩展的默认析构函数)。此处调用TraceImpl的默认析构函数,发现类只有前置声明(具体实如今Trace1.cpp文件中,main中没有引入此文件),所以不知道TraceImpl的实际大小。
问题出来了,为何须要知道TraceImpl的实际大小呢?能够认为c++中的new是malloc的封装,执行new的时候,其实就是根据类的大小malloc固定大小的空间,反之,delete也就是释放掉指定大小的空间。你不提供声明,这就让编译器很为难,只能报错了。
解决方式很简单,一切都是inline引发的,那么咱们就让析构函数outline。经过这种方式,将Trace1的析构函数实现转移至Trace1.cpp中,从而发现TraceImpl的具体实现。代码以下所示:
// Trace1.h class Trace1 : public boost::noncopyable { public: Trace1(); ~Trace1(); void test(); private: class TraceImpl; std::unique_ptr<TraceImpl> _impl; }; // Trace1.cpp class Trace1::TraceImpl { public: TraceImpl() = default; static std::string test() { return "hello trace1"; } }; Trace1::Trace1() : _impl(std::make_unique<Trace1::TraceImpl>()) { } Trace1::~Trace1() = default; void Trace1::test() { std::cout << _impl->test() << std::endl; }
如此操做,析构函数就能够看见TraceImpl的声明,因而就能正确的执行析构操做。
上文中说起了,std::unique_ptr的构造函数中,第二个入参实际上是一个仿函数,那么咱们也能够经过仿函数解决这个问题,代码以下所示:
// Trace2.h class Trace2 : public boost::noncopyable { public: Trace2(); ~Trace2() = default; void test(); private: class TraceImpl; class TraceImplDeleter { public: void operator()(TraceImpl *p); }; std::unique_ptr<TraceImpl, TraceImplDeleter> _impl; }; // Trace2.cpp class Trace2::TraceImpl { public: TraceImpl() = default; static std::string test() { return "hello trace2"; } }; void Trace2::TraceImplDeleter::operator()(Trace2::TraceImpl *p) { delete p; } Trace2::Trace2() : _impl(new Trace2::TraceImpl, Trace2::TraceImplDeleter()) { } void Trace2::test() { std::cout << _impl->test() << std::endl; }
是的,仿函数的实现置于Trace2.cpp中,完美解决问题。
不过我不喜欢这样的写法,由于无法使用std::make_unique初始化_impl,缘由就这么简单。
PS:
若是您以为个人文章对您有帮助,请关注个人微信公众号,谢谢!