C++學習筆記之pimpl用法詳解
前言
本文主要給大家介紹了關(guān)于C++中pimpl用法的相關(guān)內(nèi)容,分享出來供大家參考學習,下面話不多說了,來一起看看詳細的介紹:
C++的pImpl可以說是最常見的慣用手法了,在很多的C++項目和C++開發(fā)庫中都有所見。plmp的縮寫就是Pointer to Implementor,顧名思義就是將真正的實現(xiàn)細節(jié)的Implementor從類定義的頭文件中分離出去,公有類通過一個私有指針指向隱藏的實現(xiàn)類,是促進接口和實現(xiàn)分離的重要機制。
在C++語言中,要定義某個類型的變量或者使用類型的某個成員,就必須知道這個類的完整定義,其例外情況是:如果定義這個類型的指針,或者該類型是函數(shù)的參數(shù)或者返回類型(即使是傳值類型的),那么就可以通過前置聲明引入這個類型的名字,而不需要提供暴露其完整的類型定義,從而類型的完整定義可以被隱藏在其他hpp頭文件或者cpp實現(xiàn)文件中,而這個指針也被稱為不透明指針(opaque pointer)。通常的pImp的手法是在API的頭文件中提供接口類的定義以及實現(xiàn)類的前置聲明,實現(xiàn)類的本身定義和成員函數(shù)的實現(xiàn)都隱藏在cpp文件中去,同時為了避免實現(xiàn)類的符號污染外部名字空間,實現(xiàn)類大多作為接口類的內(nèi)部嵌套類的形式。
一、pImpl手法的優(yōu)勢和目的
1.1 信息隱蔽
私有成員完全可以隱藏在共有接口之外,尤其對于閉源API的設(shè)計尤其的適合。同時,很多代碼會應(yīng)用平臺依賴相關(guān)的宏控制,這些瑣碎的東西也完全可以隱藏在實現(xiàn)類當中,給用戶一個間接明了的使用接口再好不過了。
1.2 加速編譯
這通常是用pImpl手法的最重要的收益,稱之為編譯防火墻(compilation firewall),主要是阻斷了類的實現(xiàn)和類的實現(xiàn)兩者的編譯依賴性。這樣,類用戶不需要額外include不必要的頭文件,同時實現(xiàn)類的成員可以隨意變更,而公有類的使用者不需要重新編譯。
1.3 更好的二進制兼容性
承接上面說的,通常對一個類的修改,會影響到類的大小、對象的表示和布局等信息,那么任何該類的用戶都需要重新編譯才行。而且即使更新的是外部不可訪問的private部分,雖然從訪問性來說此時只有類成員和友元能否訪問類的私有部分,但是由于C++的特性是名字查找先于名字查找和重載解析的(即使不可訪問也會返回調(diào)用失敗,而不是視而不見),私有部分的修改也會影響到類使用者的行為,這也迫使類的使用者需要重新編譯。而對于使用pImpl手法,如果實現(xiàn)變更被限制在實現(xiàn)類中,那公有類只持有一個實現(xiàn)類的指針,所以實現(xiàn)做出重大變更的情況下,pImpl也能夠保證良好的二進制兼容性。
因此,獨立和自由是pImpl的精髓所在。
1.4 惰性分配
實現(xiàn)類可以做到按需分配或者實際使用時候再分配,從而節(jié)省資源提高響應(yīng)。如果你意識到這點了,那是很不錯的。
二、公有類和實現(xiàn)類的隔離程度
由于公有類是實現(xiàn)類的抽象,實現(xiàn)類是公有類的封裝隱藏,推薦的隔離方式是:將所有非virtual的private成員都放置到impl中去,同時將private成員函數(shù)需要調(diào)用的公有函數(shù)也放置到impl中去,virtual函數(shù)和protected的成員不應(yīng)當放到impl中去。
protected的成員放到impl中沒有任何的意義,因為protected是相對于繼承關(guān)系而生效的;同樣的,virtual成員也不應(yīng)該放到impl中去,因為virtual函數(shù)需要被繼承鏈中的派生類去override。這里需要提到,virtual函數(shù)也可以是private的,函數(shù)的virtual和access兩者是正交毫無關(guān)聯(lián)的,即使派生類無法訪問基類的虛函數(shù),但是派生類仍然可以override基類的虛函數(shù)!這引出了一個Template Method的設(shè)計模式。
將private函數(shù)需要調(diào)用到的public方法也放到impl中去,是為了避免下面所描述的back pointer帶來開銷的妥協(xié)。當然,還有一種極端的方式是除此以外將所有的public成員都丟到impl中去,那么公有類就相當于一個接口類,進而所有接口都需要一個wrapper進行調(diào)用的轉(zhuǎn)發(fā),此時公有類會實現(xiàn)的比較無趣和雜亂,而且無法被繼承復用。
三、pImpl實現(xiàn)需要注意事項
3.1 資源管理
盡可能避免的使用原始指針來創(chuàng)建和delete釋放實現(xiàn)類對象,使用boost::scoped_ptr
或者std::unique_ptr
來管理實現(xiàn)類對象,而且如果確實需要實現(xiàn)類共享,可以使用boost::shared_ptr
來管理。同時scoped_ptr、unique_ptr實現(xiàn)上要比shared_ptr高效的多。
如果使用智能指針管理實現(xiàn)類對象的話,使用unique_ptr則需要手動在實現(xiàn)文件中定義共有類的析構(gòu)函數(shù),這是因為雖然unique_ptr和shared_ptr都可以在類型不完全的情況下定義其智能指針,但是unique_ptr其析構(gòu)函數(shù)則需要具有持有類型的完全定義,而shared_ptr比較智能則沒有這個限制。
3.2 拷貝語義
pImpl最需要關(guān)注的就是共有類的復制語義,因為實現(xiàn)類是以指針的方式作為共有類的一個成員,而默認C++生成的拷貝操作只會執(zhí)行對象的淺復制,這顯然違背了pImpl的原本意圖,除非是真的想要底層共享一個實現(xiàn)對象。針對這個問題,解決方式有:
a. 禁止復制操作,將所有的復制操作定義為private的,或者繼承boost::noncopyable
,或者在新標準中將這些復制操作定義為delete的即刻;
b. 顯式定義復制語義,創(chuàng)建新的實現(xiàn)類對象,執(zhí)行深度復制操作。此處需要記住0-3-5法則哦,要么不定義拷貝、移動操作符,要定義就需要將他們?nèi)恐匦露x。
3.3 impl對公有類的反向引用
實現(xiàn)類中的私有成員如果需要訪問公有類的公共、保護的成員,就必須要能夠引用到公有類對象,實現(xiàn)其手段有:
a. impl持有一個對公有類對象的指針或者引用。雖然方便但是往往會有問題:如果持有的是引用,則拷貝賦值就難以實現(xiàn),如果持有的是指針,則需要小心指針有效性的同步負擔(比如移動操作)。
b. 推薦的方式,是impl中的這些函數(shù)都增加一個對公有類的引用或者指針,那么其調(diào)用方法類似于:
pimpl->func(this, params);
3.4 pImpl手法的缺點:
a. 該手法需要在調(diào)用和實現(xiàn)之間插入了一個指針,公有類在訪問私有成員的時候都需要增加mImpl->前綴的方式,使用、閱讀和調(diào)試都可能有所不便;
b. pImpl對拷貝操作比較敏感,要么你禁止拷貝操作,要么就需要自定義拷貝操作;
c. 編譯器將不再能夠捕獲const方法中對成員變量的修改,因為私有成員變量已經(jīng)從公有類脫離到了實現(xiàn)類當中了,公有類的const只能保護指針值本身是否改變,而不再能進一步保護其所指向的數(shù)據(jù)。如果要達到類似的保護效果,可以使用std::experimental::propagate_const
技術(shù)。
pImpl是一個很重要、實用的編程技巧,強烈建議掌握之!
總結(jié)
以上就是這篇文章的全部內(nèi)容了,希望本文的內(nèi)容對大家的學習或者工作能帶來一定的幫助,如果有疑問大家可以留言交流,謝謝大家對腳本之家的支持。
相關(guān)文章
C++11 寫一個只觸發(fā)一次槽函數(shù)的Qt connect函數(shù)
這篇文章主要為大家介紹了C++11 寫一個只觸發(fā)一次槽函數(shù)的Qt connect函數(shù)實例詳解,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進步,早日升職加薪2022-09-09C++中的動態(tài)分派在HotSpot?VM中的應(yīng)用小結(jié)
多態(tài)是面向?qū)ο缶幊陶Z言的重要特性,它允許基類的指針或引用指向派生類的對象,而在具體訪問時實現(xiàn)方法的動態(tài)綁定,這篇文章主要介紹了C++的動態(tài)分派在HotSpot?VM中的重要應(yīng)用,需要的朋友可以參考下2023-09-09C++實現(xiàn)十進制數(shù)轉(zhuǎn)換為二進制數(shù)的數(shù)學算法
這篇文章和大家分享一下我個人對十進制數(shù)轉(zhuǎn)換為二進制數(shù)的想法,目前暫時更新只整數(shù)十進制的轉(zhuǎn)換,后續(xù)會更新帶有小數(shù)的進制轉(zhuǎn)換,代碼使用c++實現(xiàn)2021-09-09