最新国产好看的视频,伊人天堂AV在线,国产Aaaaaa视频,蜜臀视频在线观看一区,人妻av色图,密臀久久久精品影片,青青视频免费观看毛片,久草在线观看视,国产三级精品色情在线

C++接口文件小技巧之PIMPL詳解

 更新時間:2023年06月18日 08:35:08   作者:Zijian/TENG  
C++ 里面有一些慣用法(idioms),如 RAII,PIMPL,copy-swap、CRTP、SFINAE 等,今天要說的是 PIMPL,即 Pointer To Implementation,指向?qū)崿F(xiàn)的指針,感興趣的可以了解一下

C++ 里面有一些慣用法(idioms),如 RAII,PIMPL,copy-swap、CRTP、SFINAE 等。今天要說的是 PIMPL,即 Pointer To Implementation,指向?qū)崿F(xiàn)的指針。

問題描述

在實際的項目中,經(jīng)常需要定義和第三方/供應(yīng)商的 C++ 接口。假如有這樣一個接口:

#include <string>
#include <list>
#include "dds.h"

class MyInterface {
   public:
    int publicApi1();
    int publicApi2();

   private:
    int privateMethod1();
    int privateMethod2();
    int privateMethod3();

   private:
    std::string name_;
    std::list<int> list_;
    DDSDomainPariciant dp_;
    DDSTopic topic_;
    DDSDataWriter dw_;
};

該接口頭文件存在以下問題:

1.暴露了 MyInterface 內(nèi)部實現(xiàn)

所有的 private/protected 的方法、成員變量都暴露給接口的使用者

2.由此帶來的另一個問題是接口不穩(wěn)定。比如我們修改類的內(nèi)部實現(xiàn),即使不改變 public 接口,接口的使用者也需要跟著更新頭文件:

  • 比如 list_ 成員之前用的是 std::list 容器,現(xiàn)在打算改用 std::vector 容器
  • 再比如,之前有 3 個 private 方法,現(xiàn)在重構(gòu)實現(xiàn)部分,拆成更多的小函數(shù)

3.增加了使用者的依賴

接口的使用者想要使用上述頭文件,必須要 #include "dds.h" 這個文件,而 "dds.h" 通常又會 #include 很多其他文件。最終的結(jié)果往往是要向接口的使用者提供很多額外的頭文件。如果將來重構(gòu),不用 DDS,改用 SOME/IP 或其他中間件,接口的使用者也要跟著改變。不僅如此,為 private 成員而額外 #include 的頭文件也會增加編譯時間

解決方案 —— PIMPL

PIMPL 就是 C++ 里專門用來解決這些問題的慣用法。PIMPL 將 MyInterface 類的具體實現(xiàn)(private/protected 方法、成員)轉(zhuǎn)移到另外一個嵌套類 Impl 中,然后利用前向聲明(forward declaration)聲明 Impl,并在原有的 MyInterface 接口類中增加一個指向 Impl 對象的指針。再次強調(diào),在 MyInterface 中的 Impl 僅僅是一個前向聲明,MyInterface 類只知道有 Impl 這么個類,但是對 Impl 有哪些方法、哪些成員變量一無所知,因此能做的事情非常有限(聲明一個指向該類的指針就是其中之一)。而這恰恰就是 PIMPL 將接口和實現(xiàn)解耦的關(guān)鍵所在。

應(yīng)用 PIMPL 后的 MyInterface.h 文件:

class MyInterface {
   public:
    MyInterface();
    ~MyInterface();
    int publicApi1();
    int publicApi2();
   private:
    struct Impl;
    Impl* impl_;
};

現(xiàn)在 MyInterface.h 接口文件變得非常清爽,看不到任何 private/protected 的方法和成員變量,也不需要 #include 任何和 private 成員相關(guān)的頭文件,隱藏實現(xiàn)細(xì)節(jié),降低使用者的依賴,提高接口穩(wěn)定性。

MyInterface.cpp

#include <string>
#include <list>
#include "dds.h"
struct MyInterface::Impl {
    int publicApi1();
    int publicApi2(int i);
    int privateMethod1();
    int privateMethod2();
    int privateMethod3();
    std::string name_;
    std::list<int> list_;
    DDSDomainPariciant dp_;
    DDSTopic topic_;
    DDSDataWriter dw_;
};
MyInterface::MyInterface() 
    : pimpl_(new Impl()) {}
MyInterface::~MyInterface() {
    delete pimpl_;
}
int MyInterface::publicApi1() {
    impl_->publicApi1();
}
int MyInterface::publicApi2(int i) {
    impl_->publicApi2(i);
}
// 其他 MyInterface::Impl 類的方法實現(xiàn)
// 原本 MyInterface 中的邏輯挪到 MyInterface::Impl 中
int MyInterface::Impl::publicApi1() {...}

可以看到,MyInterface 類的實現(xiàn)本身只是單純地將請求委托/轉(zhuǎn)發(fā)給 MyInterface::Impl 的同名方法。對于參數(shù)的傳遞,也可以適當(dāng)使用 std::move 提升效率(關(guān)于 std::move 今后也可以展開說說)。

也可以把嵌套類 MyInterface::Impl 放到單獨 MyInterfaceImpl.h/cpp 中,如此一來 MyInterface.cpp 就會變得非常簡潔,就像下面這樣:

MyInterface.cpp

#include "MyInterface.h"
#include "MyInterfaceImpl.h"
MyInterface::MyInterface() 
    : pimpl_(new Impl()) {}
MyInterface::~MyInterface() {
    delete pimpl_;
}
int MyInterface::publicApi1() {
    return impl_->publicApi1();
}
int MyInterface::publicApi2(int i) {
    return impl_->publicApi2(i);
}

MyInterfaceImpl.h

#include <string>
#include <list>
#include "dds.h"
struct MyInterface::Impl {
    int publicApi1();
    int publicApi2(int i);
    int privateMethod1();
    int privateMethod2();
    int privateMethod3();
    std::string name_;
    std::list<int> list_;
    DDSDomainPariciant dp_;
    DDSTopic topic_;
    DDSDataWriter dw_;
};

MyInterfaceImpl.cpp

#include "MyInterfaceImpl.h"
int MyInterface::Impl::publicApi1() {
    // ...
}
// 其他 MyInterface::Impl 類的方法定義

注意不要在 MyInterface.h 中 #include "MyInterfaceImpl.h",否則就前功盡棄了。

現(xiàn)代 C++ 中的 PIMPL

以上是傳統(tǒng) C++ 中的 PIMPL 的實現(xiàn),現(xiàn)代 C++ 應(yīng)盡量避免使用裸指針,而使用智能指針。具體的原因見文末補充內(nèi)容。

Impl 對象的所有權(quán)應(yīng)該是 MyInterface 獨有 ,unique_ptr 是合情合理的選擇。如果直接將上述的裸指針替換成 unique_ptr

#include <memory>
class MyInterface {
   public:
    MyInterface();
    int publicApi1();
    int publicApi2();
   private:
    struct Impl;
    std::unique_ptr<Impl> impl_;
};
// main.cpp
int main() {
    MyInterface if;
}

gcc 下會看到這樣的報錯:

/opt/compiler-explorer/gcc-13.1.0/include/c++/13.1.0/bits/unique_ptr.h: In instantiation of 'constexpr void std::default_delete<_Tp>::operator()(_Tp*) const [with _Tp = MyInterface::Impl]':
/opt/compiler-explorer/gcc-13.1.0/include/c++/13.1.0/bits/unique_ptr.h:404:17:   required from 'constexpr std::unique_ptr<_Tp, _Dp>::~unique_ptr() [with _Tp = MyInterface::Impl; _Dp = std::default_delete<MyInterface::Impl>]'
<source>:118:7:   required from here
/opt/compiler-explorer/gcc-13.1.0/include/c++/13.1.0/bits/unique_ptr.h:97:23: error: invalid application of 'sizeof' to incomplete type 'MyInterface::Impl'
   97 |         static_assert(sizeof(_Tp)>0,
      |                       ^~~~~~~~~~~

揭曉答案前,先思考一下,問題出在哪里。

問題出在 MyInterface 的析構(gòu)函數(shù)。在沒有顯式聲明析構(gòu)函數(shù)的情況下,編譯器會默認(rèn)合成一個隱式內(nèi)聯(lián)的析構(gòu)函數(shù)(編譯器在什么條件下,自動合成哪些函數(shù)也有不少學(xué)問,后面會單獨發(fā)一篇),即等效如下代碼:

class MyInterface {
   public:
    int publicApi1();
    int publicApi2();
    ~MyInterface(){} // 是實現(xiàn),不是聲明!
   private:
    struct Impl;
    std::unique_ptr<Impl> impl_;
};

在 MyInterface.h 中,編譯器會自動合成 MyInterface 的析構(gòu)函數(shù)的實現(xiàn)(而非聲明),在這個析構(gòu)函數(shù)實現(xiàn)里,會進(jìn)行以下操作:

  • 執(zhí)行空的析構(gòu)函數(shù)體
  • 按照構(gòu)造的相反順序,依次銷毀 MyInterface 的成員
  • 銷毀 unique_ptr impl_ 成員
  • 調(diào)用 unique_ptr 的析構(gòu)函數(shù)
  • unique_ptr 的析構(gòu)函數(shù)調(diào)用默認(rèn)的刪除器(delete),刪除指向的 Impl 對象

我們所看到報錯,就出在第 5 步。unique_ptr 的實現(xiàn)代碼在刪除前,會進(jìn)行 static_assert(sizeof(_Tp)>0 斷言,而編譯器執(zhí)行該斷言的時候,Impl 還是一個不完整類型(Incomplete Type)。因為編譯器此時只看到了 MyInterface::Impl 的前向聲明,還沒有看到定義,不知道 Impl 有哪些成員,也不知 Impl 類占用多大內(nèi)存,所以在進(jìn)行 sizeof(Impl) 的時候報錯。

知道了背后的原理,解決起來也很簡單,就是保證在 MyInterface 析構(gòu)函數(shù)實現(xiàn)的地方,能看到 Impl 類的定義即可:

MyInterface.h

#include <memory>
class MyInterface {
   public:
    int publicApi1();
    int publicApi2();
    MyInterface();
    ~MyInterface();  // 使用 unique_ptr 的關(guān)鍵:只聲明,不實現(xiàn)!
   private:
    struct Impl;
    std::unique_ptr<Impl> impl_;
};

MyInterface.cpp

#include <memory>
#include "MyInterface.h"
#include "MyInterfaceImpl.h"
MyInterface::MyInterface()
    : pImpl_(std::make_unique<Impl>()) {}
MyInterface::~MyInterface() = default;
int MyInterface::publicApi1() {
    return impl_->publicApi1();
}
int MyInterface::publicApi2(int i) {
    return impl_->publicApi2(i);
}

這樣,一個正確的 PIMPL 就搞定啦!雖然 PIMPL 多了一層封裝,稍微增加了一點點復(fù)雜度,但我認(rèn)為這么做是絕對的利大于弊。以一個我曾參與的項目為例,在將近一年的時間里,實現(xiàn)庫更新了很多版,但是接口文件從釋放以來一直沒變過,大大減少了和第三方/供應(yīng)商的溝通、調(diào)試成本。

最后,留一個思考題:為什么將 unique_ptr 換成 shared_ptr 不會遇到上面的 static_assert(sizeof(_Tp)>0 編譯錯誤?如果你能解釋其中的原因,那說明你對 shared_ptr、unique_ptr 的理解相當(dāng)深入了

知識補充

裸指針七宗罪

1.裸指針無法說明指向的是單個對象還是一個數(shù)組

2.裸指針無法說明使用完指針是否需要析構(gòu),即從聲明中看不出來指針是否擁有所指向的對象

3.即使知道需要析構(gòu),也不知道應(yīng)該用 delete 還是調(diào)用某個類似 deinit(p) 的函數(shù)

4.即使知道用 delete,也不知道用 delete 還是 delete[](見理由 1)

5.即使知道如何析構(gòu),還要保證在整個路徑上,剛好只調(diào)用一次析構(gòu):少調(diào)用導(dǎo)致資源泄露,調(diào)用多次將產(chǎn)生未定義行為(如同一指針 delete 兩次可能導(dǎo)致程序崩潰)

6.空懸指針(dangling pointer):對象已析構(gòu),但仍有指針指向它

7.(我自己硬湊的)使用不便:取地址、解引用、通過 -> 來訪問成員,比 . 多按兩個鍵,手指移動距離遠(yuǎn),容易按錯...

解決方案

(我自己瞎說的)能不用就不用,能用對象用對象,不要什么都無腦 new 堆上

智能指針:unique_ptr(默認(rèn)首選), shared_ptr(除非明確需要共享所有權(quán)), weak_ptr

到此這篇關(guān)于C++接口文件小技巧之PIMPL詳解的文章就介紹到這了,更多相關(guān)C++ PIMPL內(nèi)容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!

相關(guān)文章

  • C++運算符重載圖文詳解

    C++運算符重載圖文詳解

    運算符重載的方法是定義一個重載運算符的函數(shù),在需要執(zhí)行被重載的運算符時,系統(tǒng)就自動調(diào)用該函數(shù),以實現(xiàn)相應(yīng)的運算。也就是說,運算符重載是通過定義函數(shù)實現(xiàn)的
    2021-09-09
  • 利用C語言編寫一個無限循環(huán)語句

    利用C語言編寫一個無限循環(huán)語句

    這篇文章主要介紹了利用C語言編寫一個無限循環(huán)語句問題,具有很好的參考價值,希望對大家有所幫助。如有錯誤或未考慮完全的地方,望不吝賜教
    2022-11-11
  • C語言實現(xiàn)洗牌與發(fā)牌游戲

    C語言實現(xiàn)洗牌與發(fā)牌游戲

    這篇文章主要為大家詳細(xì)介紹了C語言洗牌與發(fā)牌游戲,文中示例代碼介紹的非常詳細(xì),具有一定的參考價值,感興趣的小伙伴們可以參考一下
    2020-12-12
  • Qt網(wǎng)絡(luò)編程之TCP通信及常見問題

    Qt網(wǎng)絡(luò)編程之TCP通信及常見問題

    這篇文章主要為大家詳細(xì)介紹了Qt網(wǎng)絡(luò)編程之TCP通信及常見問題,文中示例代碼介紹的非常詳細(xì),具有一定的參考價值,感興趣的小伙伴們可以參考一下
    2022-08-08
  • 淺析C++ 仿函數(shù)

    淺析C++ 仿函數(shù)

    這篇文章主要介紹了C++ 仿函數(shù)的相關(guān)資料,幫助大家更好的理解和學(xué)習(xí)c++,感興趣的朋友可以了解下
    2020-08-08
  • C++ Boost PropertyTree解析INI文件詳解

    C++ Boost PropertyTree解析INI文件詳解

    Boost PropertyTree庫不僅可以解析JSON,XML格式,還可以直接解析INI格式文件。這篇文章就是為大家介紹一下如何通過Boost PropertyTree解析INI文件,需要的可以參考一下
    2022-01-01
  • STL容器之list源碼詳細(xì)解讀

    STL容器之list源碼詳細(xì)解讀

    這篇文章主要介紹了STL容器之list源碼詳細(xì)解讀,相對于vector的連續(xù)線性空間,list就顯得更加復(fù)雜,它每插入或者刪除一個元素,就配置或釋放一個元素空間,需要的朋友可以參考下
    2024-01-01
  • C語言MFC基礎(chǔ)之計算器詳解

    C語言MFC基礎(chǔ)之計算器詳解

    這篇文章主要為大家介紹了MFC實現(xiàn)簡單的計算器,文中介紹的非常詳細(xì),具有一定的參考價值,感興趣的小伙伴們可以參考一下,希望能夠給你帶來幫助
    2021-08-08
  • C++中虛表是什么意思(概念及示例)

    C++中虛表是什么意思(概念及示例)

    虛函數(shù)表,以及虛函數(shù)指針是實現(xiàn)多態(tài)性(Polymorphism)的關(guān)鍵機制,這篇文章主要介紹了C++中虛表是什么意思(概念及示例),需要的朋友可以參考下
    2024-03-03
  • C++冒泡排序算法實例

    C++冒泡排序算法實例

    這篇文章主要介紹了C++冒泡排序算法實例,本文先是介紹了什么是冒泡排序,然后給出了實現(xiàn)代碼,需要的朋友可以參考下
    2014-10-10

最新評論

五常市| 秭归县| 喀喇沁旗| 阿城市| 禄丰县| 江都市| 青冈县| 务川| 镇远县| 武冈市| 葫芦岛市| 葫芦岛市| 萨嘎县| 鹤岗市| 丹巴县| 木兰县| 鹤山市| 板桥市| 通辽市| 永丰县| 闽侯县| 镇远县| 南木林县| 清流县| 婺源县| 延寿县| 洞头县| 衢州市| 平陆县| 阿巴嘎旗| 宿州市| 渑池县| 阿鲁科尔沁旗| 林口县| 枞阳县| 锡林浩特市| 西安市| 理塘县| 闽清县| 绥德县| 高要市|