1. 程式人生 > >c++11 條款22:當使用Pimpl(指向實現的指標)時,在實現檔案裡定義特定的成員函式

c++11 條款22:當使用Pimpl(指向實現的指標)時,在實現檔案裡定義特定的成員函式

條款22:當使用Pimpl(指向實現的指標)時,在實現檔案裡定義特定的成員函式

        假如你曾經和過多的編譯構建時間抗爭過,你應該熟悉Pimpl(指向實現的指標)這個術語。這項技術是你可以把類的資料成員替換成一個指向實現類(結構)的指標,把原來在主類中的資料成員放置到實現類中,然後通過指標間接的訪問這些資料。比如我們的Widget類是這樣的:

class Widget {                            // in header "widget.h"
public:
    Widget();
     …
private:
    std::string name;
    std::vector<double> data;
    Gadget g1, g2, g3;                    // Gadget is some user-
};                                        // defined type


        因為Widget的資料成員包括了std::string,std::vector,Gadget型別,這些型別的標頭檔案必須和Widget去編譯,那說明Widget客戶端必須#include <string>, <vector>, 以及gadget.h。這些標頭檔案增加了Widget客戶端的編譯時間,加上它們會使得客戶端依賴於那些標頭檔案的內容。假如一個頭檔案的內容改變了,Widget客戶端必須重編譯。標準標頭檔案<string>和<vector>不會經常改變,但gadget.h有可能會經常修正。

        把C++98中的Pimpl技術應用在這裡可以把Widget的資料成員替換成一個指向已宣告但未定義的結構的原始指標:

class Widget {          // still in header "widget.h"
public:
    Widget();
    ~Widget();          // dtor is needed—see below
    …
private:
    struct Impl;        // declare implementation struct
    Impl *pImpl;        // and pointer to it
};

        因為Widget沒有提及std::string,std::vector和Gadget型別,Widget客戶端不再需要#include這些型別的標頭檔案。這會加速編譯,而且意味著即使這些標頭檔案的內容發生改變,Widget客戶端也不受影響。

        一個已宣告但未定義的型別被稱作不完整型別。Widget::Impl 就是這樣的型別。對一個不完整型別你可以做的事情很少,但是宣告一個指向它的指標就是其中之一可做的事情。Pimpl技巧就利用這點。

        Pimpl技巧的第一部分是宣告一個指向不完整型別的指標的資料成員,第二部分是動態分配和析構該物件(物件裡儲存了在原始類裡的資料成員)。分配和析構的程式碼放在實現檔案裡。比如對Widget,就到到Widget.cpp裡:

#include "widget.h"     // in impl. file "widget.cpp"
#include "gadget.h"
#include <string>
#include <vector>


struct Widget::Impl {               // definition of Widget::Impl
    std::string name;               // with data members formerly
    std::vector<double> data;       // in Widget
    Gadget g1, g2, g3;
};


Widget::Widget()   // allocate data members for
: pImpl(new Impl)  // this Widget object
{}


Widget::~Widget()     // destroy data members for
{ delete pImpl; }     // this object

        這裡顯示的#include指令表明對std::string, std::vector和 Gadget的總體依賴依然存在。然而這些依賴關係以及從Widget.h(對Widget客戶端可見並被使用)轉移到了Widget.cpp(只對Widget實現者可見並被使用)。我也已經高亮標註了動態分配和析構Impl物件的程式碼。當Widget被銷燬時需要析構該物件,這是Widget解構函式所必須做的。

        但是我展示的是c++98的程式碼,這已經是上個世紀的標準了。它使用了原始指標,原始的new和delete,因此都是原始的。這一章的主旨是儘量使用靈巧指標而不用原始指標,假如我們需要的是在Widget建構函式裡動態的分配一個Widget::Impl 物件,同時析構時自動釋放物件,那麼std::unique_ptr(見條款18)恰恰就是一個我們需要的工具。用std::unique_ptr代替原始的指向Impl的指標,產生如下的程式碼,

class Widget {                     // in "widget.h"
public:
    Widget();
    …
private:

    struct Impl;
    std::unique_ptr<Impl> pImpl;   // use smart pointer
};                                 // instead of raw pointer

        實現檔案如下:

#include "widget.h"              // in "widget.cpp"
#include "gadget.h"
#include <string>
#include <vector>


struct Widget::Impl {            // as before
    std::string name;
    std::vector<double> data;
    Gadget g1, g2, g3;
};


Widget::Widget()                  // per Item 21, create
: pImpl(std::make_unique<Impl>()) // std::unique_ptr
{}                                // via std::make_unique

        你會注意到,Widget的解構函式不存在了。因為析構裡面不需要程式碼了。當std::unique_ptr被銷燬時,它會自動刪除掉它所指向的物件,因此我們不需要自己刪除任何東西。這也正是靈巧指標吸引人的地方之一:它減少了我們手動釋放資源的需求。

        這段程式碼可以通過編譯,但很可惜在少數的客戶端不能使用:

#include "widget.h"


Widget w; // error!

        錯誤資訊依賴你使用的編譯器,但大致資訊會提到關於在不完整型別上使用了sizeof或delete。這些操作不能在該型別上使用。

        使用std::unique_ptr來實現Pimpl技巧的失敗而發出告警:因為

(1)std::unique_ptr宣傳是可以支援不完整型別的,而且

(2)Pimpl技巧是std::unique_ptr最常用的場景之一。

幸運的是,使這些程式碼工作很容易,需要基本瞭解這個問題的產生原因。

        這個問題主要是w被銷燬時(比如超出範圍)所執行的程式碼引起的。被銷燬時解構函式被呼叫,在定義std::unique_ptr的類裡,我們沒有宣告解構函式,因為我們沒有程式碼要放到其中。根據編譯器會生成特定成員函式的基本規則(見條款17),編譯器為我們產生了一個解構函式。在解構函式內,編譯器插入程式碼呼叫Widget的資料成員pImpl的解構函式,pImpl是std::unique_ptr<Widget::Impl>,std::unique_ptr使用預設刪除器。這個預設刪除器會去刪除std::unique_ptr內部的原始指標,然而在刪除前,c++11中典型的實現會使用static_assert去確保原始指標沒有指向不完整型別。當編譯器為Widget w的解構函式產生程式碼時,會遇到一個static_assert失敗,於是就導致了錯誤發生。這個錯誤產生在w被銷燬處,因為Widget的解構函式和其他的編譯器產生的特殊的成員函式一樣,都是行內函數。這個編譯錯誤通常會指向w生成的程式碼行,因為正是建立物件的這行原始碼導致了隱式析構。

        為了修復這個問題,你只需要保證在生成析構std::unique<Widget::Impl>程式碼的地方,Widget::Impl是個完整型別。當型別的定義可以被看到時,型別就是完整的。而Widget::Impl是定義在Widget.cpp檔案中的。

成功編譯的關鍵是讓編譯器在widget.cpp在Widget::Impl定義之後看到Widget的解構函式的函式體。

這個很簡單,在widget.h中宣告Widget的解構函式,但是不在那裡定義它:

class Widget {                    // as before, in "widget.h"
public:
    Widget();
    ~Widget();                    // declaration only
    …
private:                          // as before
    struct Impl;
    std::unique_ptr<Impl> pImpl;
};

在widget.cpp中,Impl的定義之後再定義該解構函式:

#include "widget.h"           // as before, in "widget.cpp"
#include "gadget.h"
#include <string>
#include <vector>
struct Widget::Impl {         // as before, definition of

std::string name;             // Widget::Impl
std::vector<double> data;
Gadget g1, g2, g3;
};


Widget::Widget()                                // as before
: pImpl(std::make_unique<Impl>())
{}
Widget::~Widget()                               // ~Widget definition
{}

        這端程式碼工作正常,程式碼也是最簡短的。但如果你想強調編譯器生成的析構將會做正確的事情,而你這裡宣告它只是要使得它的定義出現在Widget的實現檔案中,那麼你可以在解構函式的函式體後寫上“=default”:

Widget::~Widget() = default; // same effect as above

        使用Pimpl技巧的類很天然的支援move操作,因為編譯器生成的操作完全符合預期:在一個std::unique_ptr上執行move。正如條款17解釋的,在Widget裡聲明瞭解構函式會阻止編譯器產生move操作程式碼,因此,假如你需要支援move操作,你必須自己宣告該函式。既然編譯器產生的版本是正確的,你很有可能如下實現:

class Widget {                  // still in
public:                         // "widget.h"
    Widget();
    ~Widget();
    Widget(Widget&& rhs) = default;              // right idea,
    Widget& operator=(Widget&& rhs) = default;   // wrong code!
    …
private:                                         // as before
    struct Impl;
    std::unique_ptr<Impl> pImpl;
};

        這個實現會導致和類裡面沒有解構函式一樣的問題,編譯器生成的move賦值操作需要在重分配前消毀掉pImpl指向的物件,但是在Widget的標頭檔案裡,pImpl指向的是一個不完整型別。move建構函式的情況有所不同,問題是編譯器產生的程式碼在move建構函式中去銷燬pImpl時會產生異常,而且銷燬pImpl需要完整型別的Impl。

        因為產生原因相同,所以修復辦法也一樣。把move操作函式的定義放到實現檔案裡:

class Widget {                 // still in "widget.h"
public:
    Widget();
    ~Widget();

    Widget(Widget&& rhs);             // declarations
    Widget& operator=(Widget&& rhs);  // only
    …
private: // as before
    struct Impl;
    std::unique_ptr<Impl> pImpl;
};


#include <string>                                // as before,
…                                                // in "widget.cpp"
struct Widget::Impl { … };                       // as before
Widget::Widget() // as before
: pImpl(std::make_unique<Impl>())
{}
Widget::~Widget() = default;                     // as before


Widget::Widget(Widget&& rhs) = default;                     // defini-
Widget& Widget::operator=(Widget&& rhs) = default;  // tions

        Pimpl技巧一是應用可以減少類實現和類使用者之間依賴關係的方法,但是理論上,Plimpl技巧並不能改變類本身。最初的Widget類包含了std::string,std::vector以及Gadget資料成員,我們假設Gadget類象std::string和std::vector一樣也可以被拷貝,那麼很自然的Widget類也會支援copy操作。我們必須自己寫這些函式,因為(1)編譯器不會給像std::unique_ptr一樣的move-only型別產生copy函式,(2)即使產生了,那麼產生的函式也只是拷貝std::unique_ptr(也就是淺拷貝),而我們希望的是拷貝指標指向的內容(也就是執行深拷貝)。

        根據我們目前熟悉的慣例,我們在標頭檔案裡宣告在cpp檔案裡實現它們:

class Widget {                                   // still in "widget.h"
public:
    …                                             // other funcs, as before
    Widget(const Widget& rhs);                    // declarations
    Widget& operator=(const Widget& rhs);         // only


private:                                   // as before
    struct Impl;
    std::unique_ptr<Impl> pImpl;
};


#include "widget.h"                // as before,
…                                  // in "widget.cpp"

struct Widget::Impl { … };        // as before

Widget::~Widget() = default;       // other funcs, as before

Widget::Widget(const Widget& rhs)                // copy ctor
: pImpl(std::make_unique<Impl>(*rhs.pImpl))
{}

Widget& Widget::operator=(const Widget& rhs)     // copy operator=
{
    *pImpl = *rhs.pImpl;
    return *this;
}

        兩個實現都很常規,這兩個函式我們都是簡單的拷貝Impl結構的域,從源物件(rhs)到目的物件(*this)。我們並沒有一個個的拷貝域,是因為我們利用了編譯器會為Impl類構造拷貝函式,這些拷貝函式會自動拷貝這些域的。於是我們通過呼叫Widget::Impl的編譯器生成的拷貝函式去實現Widget的拷貝操作。在這個拷貝建構函式中,我們注意到還是遵循了條款21的建議,儘量用std::make_unique而不直接用new。

        為了實現Pimpl技巧,我們使用了std::unique_ptr靈巧指標,因為在物件(這裡的Widget)中的pImpl指標對於相關的實現物件(這裡的Widget::Impl物件)獨享所有權的。更有趣的是如果我們這裡用std::share_ptr來代替std::unique_ptr實現pImpl,會發現本款的建議不再適用。不需要再宣告Widget的解構函式,也不需要使用者宣告的解構函式,編譯器會很自然的產生一個move操作,會精確的按我們想要的去工作。這裡我們看看widget.h裡的程式碼:

class Widget {          // in "widget.h"
public:
    Widget();
    …                       // no declarations for dtor
                            // or move operations
private:
    struct Impl;
    std::shared_ptr<Impl> pImpl; // std::shared_ptr
};                               // instead of std::unique_ptr

        客戶端程式碼會#includes widget.h,

Widget w1;
auto w2(std::move(w1));       // move-construct w2
w1 = std::move(w2);           // move-assign w1

        編譯正常,執行也如我們所期望:w1預設構造,值被移動到w2,然後值又被移動會w1,最後w1和w2都被銷燬(這會導致Widget::Impl類物件被銷燬)。

        對於實現pImpl指標來說使用std::unique_ptr和std::shared_ptr的區別在於這兩種靈巧指標對於定製刪除器的支援不同。對於std::unique_ptr,刪除器的型別是指標型別的一部分,這使得編譯器會產生更小尺寸以及更高效的程式碼,當然產生的結果也是當編譯器產生特定函式時(析構或移動函式),被指向的型別必須是完整的。對於std::shared_ptr,刪除器的型別不是指標型別的一部分,這需要更大尺寸的執行時資料結構以及也許更慢的速度,但被指向的型別卻不是必須的。

        對於Pimpl技巧來說,在std::unique_ptr和std::shared_ptr的特性之間並沒有一個妥協性,因為類之間必然像Widget和Widget::Impl之間的關係是獨享所有權的,因此這裡選擇std::unique_ptr是更適合的。然而在其他某些情況下,存在共享所有權(因此std::shared_ptr因此更適合),就沒有必要使用std::unique_ptr 。

需要記住的事情:

1.Pimpl技巧通過減少類的使用者和類實現之間的依賴而減少了編譯次數。

2.對於std::unique_ptr來實現pImpl指標,在檔案中定義特定函式,在cpp檔案中實現,即使預設的函式是可用的也要這樣做。

3.上述建議適用於std::unique_ptr,但不適用於std::shared_ptr。

相關推薦

c++11 條款22使用Pimpl指向實現指標實現檔案定義特定成員函式

條款22:當使用Pimpl(指向實現的指標)時,在實現檔案裡定義特定的成員函式         假如你曾經和過多的編譯構建時間抗爭過,你應該熟悉Pimpl(指向實現的指標)這個術語。這項技術是你可以把類的資料成員替換成一個指向實現類(結構)的指標,把原來在主類中的資料成員

C++11 條款1理解模板型別推導

前言 c++98有單獨一套型別推導規則:適用於函式模板。c++11修改了這套規則並且增加了兩個,一個是auto,一個是decltype。c++14擴充套件了auto和decltype使用的場景。隨著型別推導在應用程式中的使用逐步增加,你可以從那些明顯或冗餘的型別拼寫中

使用application作用域實現用戶重復登錄擠掉原來的用戶

ont 必須 用戶名 使用 執行 gets quest return http 使用application作用域實現:當用戶重復登錄時,擠掉原來的用戶 一、實現思想 1.application(ServletContext)是保存在服務器端的作用域,我們在applicati

技術團隊所有需求都是第一優先順序你該怎麼辦?

技術團隊做專案需求的工作過程中,經常會出現一些反覆不斷的問題,這些問題會嚴重影響團隊的工作效率,同時也會給團隊的士氣帶來重大的影響。接下來,我們來討論一下這些問題發生的具體場景,造成的問題原因,以及如何預防和解決這些問題方法技巧。 今天來看第一個常見的問題:當所有的需求或任務都是第一優先順序的時候,你該怎麼

第十五週實驗報告一實現氣泡排序演算法並將之定義為一個函式

  第15週報告1: 實驗目的:學會氣泡排序演算法 實驗內容:實現氣泡排序演算法,並將之定義為一個函式 * 程式頭部註釋開始 * 程式的版權和版本宣告部分 * Copyright (c) 2011, 煙臺大學計算機學院學生 * All rights reserved. *

effective c++條款22成員變數宣告為private

將成員變數宣告為private的三大理由: 1. 提供語法一致性:  如果將所有的變數都宣告為private,那麼當其他人使用這個類時,就不用糾結是以函式方式呼叫還是變數方式呼叫,更加節省時間。 #include <iostream> using namespa

C++11新特性Lambda函式匿名函式

基本的Lambda函式 我們可以這樣定義一個Lambda函式: #include <iostream> using namespace std; int main() { auto func = [] () { c

Guru of the Week 條款22物件的生存期第一部分

GotW #22 Object Lifetimes – Part I<?xml:namespace prefix = o ns = "urn:schemas-microsoft-com:office:office" /> 著者:Herb Sutter 翻譯:K ]

C++11嚐鮮Variadic Function Templates帶變長引數的函式模板

程式碼1 #include <iostream> #include <string> #include <boost/ref.hpp> #include <b

讀書筆記_Effective C++_條款C++視為一個語言聯邦

編程 pri 來看 讀書 由來 c++程序 一個 函數指針 集成 C++起源於C,最初的名稱為C with Classes,意為帶類的C語言,然而,隨著C++的不斷發展和壯大,在很多功能上已經遠遠超越了C,甚至一些C++程序員反過來看C代碼會覺得不習慣。 C++可以看成由

【TOJ 5255】C++實驗三角形面積海倫公式

esc man opera time public 三角形面積 AC pac 公式 描述 實現C++三角形類,其中包含3個點(CPoint類型),並完成求面積。主函數裏的代碼已經給出,請補充完整,提交時請勿包含已經給出的代碼。 int main() { CPoint p

c#設計模式系列命令模式Command Pattern

為我 pattern 代碼 spa pro round 產生 技術分享 image 引言 命令模式,我感覺“命令”就是任務,執行了命令就完成了一個任務。或者說,命令是任務,我們再從這個名字上並不知道命令的發出者和接受者分別是誰,為什麽呢?因為我們並不關心他們是誰,發出命令

Effective Java 第三版讀書筆記——條款2構造器引數太多時考慮使用 builder 模式

靜態工廠方法和構造器都有一個限制:不能很好地支援可選引數(optional parameters)很多的類。考慮一個代表包裝食品上營養成分標籤的類:這些標籤有幾個必需的屬性(每份建議攝入量、每個包裝所含的份數、每份的卡路里)和超過二十個可選的屬性(總脂肪、飽和脂肪、反式脂肪、鈉等等)。應該為

左轉待轉區----同向直行訊號燈綠燈亮左轉彎的車輛進入左轉待轉區等候放行訊號即使此時左轉彎燈是紅色的 注意直行紅燈時候禁止進入

左轉待轉區是什麼?不要被扣6分才後悔! from:https://www.sohu.com/a/145066213_632210   一、什麼是左轉待轉區? 從字面上看,“左轉待轉區”的意思,就是“等待左轉彎的區域”。下圖中白色虛框線就是“左轉彎待轉區”:   “左轉彎待轉區”的道路一

15、【C++】C++11新特性Lamda表示式/可變引數模板

一、Lamda表示式     Lamda表示式是C++11中引入的一項新技術,利用Lamda表示式可以編寫內嵌的匿名函式,用以替換獨立函式或者函式物件,並且使得程式碼更可讀。是一種匿名函式,即沒有函式名的函式;Lamda函式的語法定義如下: [capture] :捕捉

C語言程式設計現代方法第2版K.N.King 著》學習筆記一C語言概述

1.1 C語言的歷史 1.1.1 起源 C語言是美國貝爾實驗室的 Dennis Ritchie、Ken Thompson 等人為開發 UNIX 作業系統而於 1972 年設計的一種計算機程式語言。

C語言程式設計現代方法第2版K.N.King 著》學習筆記三C語言基本概念2

2.3 註釋 每一個程式都應該包含識別資訊,即程式名、編寫日期、作者、程式的用途以及其他相關資訊。C語言把這類資訊放在註釋(comment)中。 符號 /* 標記註釋的開始,而符號 */ 則標記註釋

C語言程式設計現代方法第2版K.N.King 著》學習筆記四C語言基本概念3

2.5 讀入輸入 為了獲取輸入,就要用到 scanf 函式。它是C函式庫中與 printf 相對應的函式。scanf 中的字母 f 和 printf 中的字母 f 含義相同,都是表示“格式化”的意思