亚洲乱码中文字幕综合,中国熟女仑乱hd,亚洲精品乱拍国产一区二区三区,一本大道卡一卡二卡三乱码全集资源,又粗又黄又硬又爽的免费视频

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)的指針。

問題描述

在實(shí)際的項(xiàng)目中,經(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)部實(shí)現(xiàn)。

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

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

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

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

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

解決方案 —— PIMPL

PIMPL 就是 C++ 里專門用來解決這些問題的慣用法。PIMPL 將 MyInterface 類的具體實(shí)現(xiàn)(private/protected 方法、成員)轉(zhuǎn)移到另外一個嵌套類 Impl 中,然后利用前向聲明(forward declaration)聲明 Impl,并在原有的 MyInterface 接口類中增加一個指向 Impl 對象的指針。再次強(qiáng)調(diào),在 MyInterface 中的 Impl 僅僅是一個前向聲明,MyInterface 類只知道有 Impl 這么個類,但是對 Impl 有哪些方法、哪些成員變量一無所知,因此能做的事情非常有限(聲明一個指向該類的指針就是其中之一)。而這恰恰就是 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)本身只是單純地將請求委托/轉(zhuǎn)發(fā)給 MyInterface::Impl 的同名方法。對于參數(shù)的傳遞,也可以適當(dāng)使用 std::move 提升效率(關(guān)于 std::move 今后也可以展開說說)。

也可以把嵌套類 MyInterface::Impl 放到單獨(dú) 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 的實(shí)現(xiàn),現(xiàn)代 C++ 應(yīng)盡量避免使用裸指針,而使用智能指針。具體的原因見文末補(bǔ)充內(nèi)容。

Impl 對象的所有權(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 下會看到這樣的報(bào)錯:

/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é)問,后面會單獨(dú)發(fā)一篇),即等效如下代碼:

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

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

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

知道了背后的原理,解決起來也很簡單,就是保證在 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);
}

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

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

知識補(bǔ)充

裸指針七宗罪

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++ 實(shí)現(xiàn)文件逐行讀取與字符匹配

    c++ 實(shí)現(xiàn)文件逐行讀取與字符匹配

    這里嘗試通過C++來實(shí)現(xiàn)一個文件IO的功能,看看是否能夠比python的表現(xiàn)更好一些,感興趣的朋友可以參考下
    2021-05-05
  • C++20 新特性 協(xié)程 Coroutines(2)

    C++20 新特性 協(xié)程 Coroutines(2)

    上篇文章簡單給大介紹了 C++20 特性 協(xié)程 Coroutines co_yield 和 co_return 那么這篇文章繼續(xù)給大家介紹C++20 的新特性協(xié)程 Coroutines co_await,需要的朋友可以參考一下
    2021-10-10
  • 一文詳解C++17中的結(jié)構(gòu)化綁定

    一文詳解C++17中的結(jié)構(gòu)化綁定

    C++17中的結(jié)構(gòu)化綁定(structured binding)是指將指定名稱綁定到初始化程序的子對象或元素,本文主要來和大家聊聊C++17中結(jié)構(gòu)化綁定的實(shí)現(xiàn),感興趣的小伙伴可以了解下
    2023-12-12
  • 教你使用Matlab制作圖形驗(yàn)證碼生成器(app designer)

    教你使用Matlab制作圖形驗(yàn)證碼生成器(app designer)

    這篇文章主要和大家分享如何利用Matlab制作一款圖形驗(yàn)證碼生成器,文中的實(shí)現(xiàn)步驟講解詳細(xì),感興趣的小伙伴可以跟隨小編動手試一試
    2022-02-02
  • C++中引用的使用總結(jié)

    C++中引用的使用總結(jié)

    以下是對C++中引用的使用進(jìn)行了詳細(xì)的總結(jié)介紹,需要的朋友可以過來參考下,希望對大家有所幫助
    2013-10-10
  • 詳解C++17中類模板參數(shù)推導(dǎo)的使用

    詳解C++17中類模板參數(shù)推導(dǎo)的使用

    自C++17起就通過使用類模板參數(shù)推導(dǎo),只要編譯器能根據(jù)初始值推導(dǎo)出所有模板參數(shù),那么就可以不指明參數(shù),下面我們就來看看C++17中類模板參數(shù)推導(dǎo)的具體使用吧
    2024-03-03
  • c語言實(shí)現(xiàn)的幾種常用排序算法

    c語言實(shí)現(xiàn)的幾種常用排序算法

    C,語言常用的排序方法有很多種。比如說冒泡排序,直接交換排序,直接選擇排序,直接插入排序,二分插入排序,快速排序,歸并排序等等,下面這篇文章主要給大家介紹了關(guān)于c語言實(shí)現(xiàn)幾種常用的排序算法,需要的朋友可以參考下
    2021-06-06
  • C語言結(jié)構(gòu)體的全方面解讀

    C語言結(jié)構(gòu)體的全方面解讀

    C 數(shù)組允許定義可存儲相同類型數(shù)據(jù)項(xiàng)的變量,結(jié)構(gòu)是 C 編程中另一種用戶自定義的可用的數(shù)據(jù)類型,它允許你存儲不同類型的數(shù)據(jù)項(xiàng)
    2021-10-10
  • C++文件關(guān)鍵詞快速定位出現(xiàn)的行號實(shí)現(xiàn)高效搜索

    C++文件關(guān)鍵詞快速定位出現(xiàn)的行號實(shí)現(xiàn)高效搜索

    這篇文章主要為大家介紹了C++文件關(guān)鍵詞快速定位出現(xiàn)的行號實(shí)現(xiàn)高效搜索,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪
    2023-10-10
  • 淺談C++中對象的復(fù)制與對象之間的相互賦值

    淺談C++中對象的復(fù)制與對象之間的相互賦值

    這篇文章主要介紹了淺談C++中對象的復(fù)制與對象之間的相互賦值,是C語言入門學(xué)習(xí)中的基礎(chǔ)知識,需要的朋友可以參考下
    2015-09-09

最新評論