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:
如果您覺得我的文章對您有幫助,請關注我的微信公眾號,謝謝!
轉載請註明出處,本文鏈接:https://www.uj5u.com/houduan/82021.html
標籤:C++

