Impl模式早就有過接觸(本文特指通過指針完成impl),我曉得它具有以下優點:
- 減少頭文件暴露出來的非必要內部類(提供靜態庫,動態庫時尤其重要);
- 減小文件間的編譯依存關系,大型代碼庫的編譯時間就不會那么折磨人了。
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;
}
很無情,我遇到了錯誤,錯誤如下所示:

為什么會這樣呢,報錯信息提示TraceImpl是一個不完整的類型。
其實,就是編譯器看到TraceImpl,無法在編譯期間確定TraceImpl的大小。此處我們使用的是std::unique_ptr,其中存放的是一個指針,沒必要知道TraceImpl的具體大小(換成std::shared_ptr就不會這個報錯)。
錯誤分析
往上看報錯信息,發現std::unique_ptr的析構函數有點意思:
/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:
如果您覺得我的文章對您有幫助,請關注我的微信公眾號,謝謝!

