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:
若是您以爲個人文章對您有幫助,請關注個人微信公衆號,謝謝!