到底是什麼毀了個人impl實現

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

相關文章
相關標籤/搜索