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

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

 更新時(shí)間: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)的指針。

問題描述

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

#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)部實(shí)現(xiàn)

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

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

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

3.增加了使用者的依賴。

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

解決方案 —— PIMPL

PIMPL 就是 C++ 里專門用來解決這些問題的慣用法。PIMPL 將 MyInterface 類的具體實(shí)現(xiàn)(private/protected 方法、成員)轉(zhuǎn)移到另外一個(gè)嵌套類 Impl 中,然后利用前向聲明(forward declaration)聲明 Impl,并在原有的 MyInterface 接口類中增加一個(gè)指向 Impl 對(duì)象的指針。再次強(qiáng)調(diào),在 MyInterface 中的 Impl 僅僅是一個(gè)前向聲明,MyInterface 類只知道有 Impl 這么個(gè)類,但是對(duì) Impl 有哪些方法、哪些成員變量一無所知,因此能做的事情非常有限(聲明一個(gè)指向該類的指針就是其中之一)。而這恰恰就是 PIMPL 將接口和實(shí)現(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)的頭文件,隱藏實(shí)現(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 類的方法實(shí)現(xiàn)
// 原本 MyInterface 中的邏輯挪到 MyInterface::Impl 中
int MyInterface::Impl::publicApi1() {...}

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

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

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 的實(shí)現(xiàn),現(xiàn)代 C++ 應(yīng)盡量避免使用裸指針,而使用智能指針。具體的原因見文末補(bǔ)充內(nèi)容。

Impl 對(duì)象的所有權(quán)應(yīng)該是 MyInterface 獨(dú)有 ,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 下會(huì)看到這樣的報(bào)錯(cuò):

/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ù)的情況下,編譯器會(huì)默認(rèn)合成一個(gè)隱式內(nèi)聯(lián)的析構(gòu)函數(shù)(編譯器在什么條件下,自動(dòng)合成哪些函數(shù)也有不少學(xué)問,后面會(huì)單獨(dú)發(fā)一篇),即等效如下代碼:

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

在 MyInterface.h 中,編譯器會(huì)自動(dòng)合成 MyInterface 的析構(gòu)函數(shù)的實(shí)現(xiàn)(而非聲明),在這個(gè)析構(gòu)函數(shù)實(shí)現(xiàn)里,會(huì)進(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 對(duì)象

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

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

MyInterface.h

#include <memory>
class MyInterface {
   public:
    int publicApi1();
    int publicApi2();
    MyInterface();
    ~MyInterface();  // 使用 unique_ptr 的關(guān)鍵:只聲明,不實(shí)現(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);
}

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

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

知識(shí)補(bǔ)充

裸指針七宗罪

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

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

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

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

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

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

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

解決方案

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

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

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

相關(guān)文章

  • C++運(yùn)算符重載圖文詳解

    C++運(yùn)算符重載圖文詳解

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

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

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

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

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

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

    這篇文章主要為大家詳細(xì)介紹了Qt網(wǎng)絡(luò)編程之TCP通信及常見問題,文中示例代碼介紹的非常詳細(xì),具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下
    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庫(kù)不僅可以解析JSON,XML格式,還可以直接解析INI格式文件。這篇文章就是為大家介紹一下如何通過Boost PropertyTree解析INI文件,需要的可以參考一下
    2022-01-01
  • STL容器之list源碼詳細(xì)解讀

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

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

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

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

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

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

    C++冒泡排序算法實(shí)例

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

最新評(píng)論

汨罗市| 水城县| 虞城县| 丰原市| 灌云县| 景泰县| 仁布县| 子洲县| 西吉县| 伊宁市| 万安县| 汝州市| 邯郸市| 荆门市| 望谟县| 鹤庆县| 青浦区| 洮南市| 晴隆县| 汉源县| 孝昌县| 台北县| 凤庆县| 镇宁| 施甸县| 平遥县| 攀枝花市| 年辖:市辖区| 韶山市| 康平县| 平乡县| 青铜峡市| 安阳县| 哈密市| 沁阳市| 博爱县| 札达县| 缙云县| 彭水| 南昌市| 香河县|