Brief Notes of 《Effective C++》

本文為學(xué)習(xí)《Effective C++》各個(gè)條款之后的一點(diǎn)概要式的總結(jié)。
github博客地址

條款2 盡量以const, enum, inline替代#define

  • 寧可用編譯器替代預(yù)處理器。以#define定義的記號是不會記錄到符號表中的;
  • #define沒有封裝性可言。
  • enum hack。enum {tmp=5};對應(yīng)的tmp一定在編譯期就可以得到并且不會導(dǎo)致非必要的內(nèi)存分配。

條款3 盡可能使用const

  • 調(diào)用const成員函數(shù)以實(shí)現(xiàn)孿生non-const成員函數(shù)。通過使用const_caststatic_cast來達(dá)到目的,優(yōu)點(diǎn)是避免了代碼重復(fù)。
  • 調(diào)用non-const成員函數(shù)實(shí)現(xiàn)const成員函數(shù)是錯誤的。因?yàn)檫@破壞了const的語義約束。

條款5 了解C++默認(rèn)編寫并調(diào)用哪些函數(shù)

  • 如果自定義了需要實(shí)參的構(gòu)造函數(shù),則編譯器不會自動生成default ctor
  • 如果class內(nèi)部包含有帶有&引用類型或者const常量類型,則編譯器不會自動生成copy assignment;因?yàn)榫幾g器不知道該怎么處理

條款7 為多態(tài)基類聲明virtual析構(gòu)函數(shù)

  • 每一個(gè)帶有virtual函數(shù)的class都擁有一個(gè)指向virtual table的指針,virtual table中包含了所有對應(yīng)virtual函數(shù)的函數(shù)指針
  • 不要嘗試?yán)^承任何標(biāo)準(zhǔn)庫容器(比如std::string),因?yàn)樗鼈兌紱]有virtual dtor。這會導(dǎo)致未定義行為
  • 沒有多態(tài)性質(zhì)的base class也不要聲明virtual dtor,比如說boost::noncopyable,virtual并無必要,且浪費(fèi)空間的
  • 如果明確了一個(gè)類具有多態(tài)性質(zhì),且作為base class使用,則應(yīng)該聲明virtual dtor

條款8 別讓異常逃離析構(gòu)函數(shù)

  • 絕對不能讓dtor吐出異常,因?yàn)楹芸赡軙斐少Y源泄露。對于有可能在dtor中發(fā)生的異常,應(yīng)該將其吞下或者提前終止程序
  • 更合適的做法是為客戶代碼提供一個(gè)接口,使得客戶有機(jī)會去處理可能發(fā)生的異常

條款11 在operator=中處理“自我賦值”

核心其實(shí)就是不能讓指針指向一個(gè)未獲取的資源;存在3類方法,各有各的優(yōu)勢

  • 賦值之前先比較lhs和rhs的地址是否相同,如果相同,則直接返回;
  • 先記住之前本身的資源(可以設(shè)一個(gè)pOrigin指針指向舊資源),隨后拷貝一份rhs的資源,并令lhs指向新資源,最后再釋放掉lhs的舊資源(即delete pOrigin)(這其實(shí)就是copy and swap的步驟...);
  • copy and swap。先拷貝rhs指向的資源,再令lhs指向的資源和這份拷貝之后的資源進(jìn)行交換;

條款12 復(fù)制對象是勿忘其每一個(gè)成分

  • 自行編寫copy ctor或者operator=是一項(xiàng)重大的責(zé)任,因?yàn)橐紤]到各種細(xì)節(jié)。而也正是因?yàn)檫@樣的原因,當(dāng)自行編寫時(shí),編譯器會認(rèn)定你是一個(gè)足夠強(qiáng)大的程序員,因此不會對自定義copy ctor和operator=的不好的地方做出任何警告;
  • 確保每一個(gè)成員變量都被正確拷貝;
  • 當(dāng)目標(biāo)是derived class時(shí),其base class的成員變量也要被正確拷貝。這需要通過調(diào)用base class的copy ctor和operator=來實(shí)現(xiàn);
  • 切記,copy ctor和operator=不能相互調(diào)用。這從語義上就行不通

條款14 在資源管理類中小心copying行為

對RAII對象執(zhí)行復(fù)制,是需要萬分小心的行為,因?yàn)樗婕暗降馁Y源的最佳處理方式不甚相同;常見的方式包括:

  • 禁止復(fù)制。很多情況下這是比較科學(xué)的做法,因?yàn)樾袨楸憩F(xiàn)的像指針這樣的數(shù)據(jù)類型是不應(yīng)該重復(fù)進(jìn)行delete的;如果不禁止復(fù)制,則必須做到對指涉到的資源也進(jìn)行復(fù)制;
  • 引用計(jì)數(shù)。不多說了,就是智能指針那一套;

條款15 在資源管理類中提供對原始資源的訪問

  • 諸如std::shared_ptrstd::unique_ptr都會提供get()成員函數(shù)來訪問其指涉的底層資源;這不是破壞封裝性,而僅僅是一種接口風(fēng)格;
  • 訪問底層資源的接口,一般而言就兩種:①get()這樣的成員函數(shù),②隱式轉(zhuǎn)換。一般來說還是①更好一點(diǎn),因?yàn)楦踩?/li>

條款16 以獨(dú)立語句將newed對象置入智能指針

  • 本條款在《Effective Modern C++》中也有講述;
  • 核心的一點(diǎn)就是在單條語句內(nèi),編譯器是有著重新編排執(zhí)行順序的自由的;
  • 因此,諸如std::shared_ptr<XXX> sp(new XXX);這樣的語句應(yīng)該單獨(dú)成句,而不應(yīng)該嵌入到其他語句中;
  • 其實(shí)現(xiàn)代C++的話,更好的做法是使用std::make_shared或者std::make_unique;它們使用完美轉(zhuǎn)發(fā),且很安全;

條款19 設(shè)計(jì)class猶如設(shè)計(jì)type

不多說了,在編寫類代碼的時(shí)候多看看本條款,思考條款中列出的問題;

條款23 寧以non-member、non-friend替換member 函數(shù)

  • 要理解這個(gè)條款,就得明確namespace的作用:①可以跨越多個(gè)源碼文件;②在實(shí)現(xiàn)類似于utility所提供的功能時(shí),更具有優(yōu)勢(因?yàn)檎Z義更清晰);③在提供了所需功能基礎(chǔ)上達(dá)到編譯依賴最低,封裝性最好
  • 書中所舉的例子:任務(wù)是調(diào)用class中的三個(gè)成員函數(shù)。那么方法大致為兩種:①再寫一個(gè)成員函數(shù),內(nèi)容就是調(diào)用那三個(gè)函數(shù);②將新的函數(shù)放在class的外部(非成員函數(shù)),但位于同一個(gè)namespace中;
  • 基于上面所陳述的原因,使用第二個(gè)方法是更好的方式

條款24 若所有參數(shù)皆需要類型轉(zhuǎn)換,請為此采用non-member函數(shù)

  • member函數(shù)的反面是non-member,而不是friend;friend在OOP中能避免則避免,因?yàn)樘茐姆庋b性了
  • 只有當(dāng)參數(shù)被置于參數(shù)列時(shí),這個(gè)參數(shù)才是隱式類型轉(zhuǎn)換的合格參與者;也就是說,當(dāng)調(diào)用成員函數(shù)時(shí),lhs實(shí)際上沒有被置于參數(shù)列中,而是this

條款26 盡可能延后變量定義式的出現(xiàn)時(shí)間

  • 應(yīng)該盡可能在要用到某個(gè)變量的時(shí)候才去定義它(這很顯然嘛)
  • 關(guān)于循環(huán)體中的變量的下述兩種定義方式,一般情況下,除非明確知道賦值操作的消耗小于構(gòu)造加析構(gòu)的時(shí)候才使用第一種;因?yàn)榈谝环N方式擴(kuò)大了變量的生命期;
// 第一種
{
  ...
  Weight tmp;
  for(int i = 0; i < N; ++i){
    tmp = Weight(i);
  }
}
// 第二種
{
  for(int i = 0; i < N; ++i){
    Weight tmp= Weight(i);
    ...
  }
}

條款29 為“異常安全”而努力是值得的

  • 所謂的異常安全函數(shù),其實(shí)就是發(fā)生異常也不會導(dǎo)致資源泄露數(shù)據(jù)敗壞;包括三類:
    • 基本保證:如果函數(shù)發(fā)生異常,則對應(yīng)的對象不一定還能還原為調(diào)用前的狀態(tài),但至少保證還是正??捎?/strong>的;
    • 強(qiáng)烈保證:即使函數(shù)發(fā)生異常,對象還是能夠還原為原來的狀態(tài),即只有兩種狀態(tài):成功調(diào)用不調(diào)用;這通常通過copy-and-swap來實(shí)現(xiàn),即先將原來的對象復(fù)制一個(gè)副本,隨后對副本執(zhí)行相應(yīng)的改變,如果執(zhí)行成功,則原對象和副本執(zhí)行swap;如果發(fā)生異常,原對象也未發(fā)生任何改變
    • 不拋擲(nothrow)保證:即保證函數(shù)不發(fā)生異常;這通常辦不到。。。只要涉及到了動態(tài)內(nèi)存的分配,都是有可能發(fā)生異常的
  • 可以看出,級別越高,其實(shí)實(shí)現(xiàn)是越困難的,并且?guī)淼拈_銷也會越高;因此,應(yīng)該挑選的是現(xiàn)實(shí)可實(shí)施下的最高等級
  • 異常安全性是遵循木桶原理的,只要函數(shù)調(diào)用了等級較低的函數(shù),那么它的異常安全性也會降低

條款30 透徹了解inlining的里里外外

  • inline在大多數(shù)C++程序中都是編譯期行為;
  • inline僅僅是一個(gè)申請,并不保證一定會內(nèi)聯(lián);
  • 是否真正內(nèi)聯(lián)還取決于函數(shù)的調(diào)用方式;(如果以函數(shù)指針進(jìn)行調(diào)用,那么就不可能被內(nèi)聯(lián)了);
  • inline的優(yōu)勢是避免調(diào)用開銷,但也存在以下問題:
    • 代碼膨脹:畢竟,如果在多處都調(diào)用了該函數(shù),那么就會有多份該函數(shù)體的副本;
    • 編譯依賴:如果inline函數(shù)發(fā)生了改變,那么所有客戶代碼都必須重新編譯;反之,如果不是內(nèi)聯(lián)的,那么僅僅重新鏈接一下就行

條款31 將文件間的編譯依存關(guān)系降至最低

C++中降低文件間的編譯依賴,主要就是兩種手段:handle class以及interface class

  • 如果客戶代碼所使用的的頭文件中,直接包含的是要使用的class的具體實(shí)現(xiàn)(包括各個(gè)函數(shù)定義),那么就形成了依賴關(guān)系;
  • 所謂依賴關(guān)系,就是指,只要一個(gè)class改變了一點(diǎn)點(diǎn)實(shí)現(xiàn),那么所有使用它的客戶代碼都需要重新編譯;
  • handle class
    • 所謂的handle class,實(shí)際上意味著一個(gè)負(fù)責(zé)聲明的class和一個(gè)負(fù)責(zé)具體實(shí)現(xiàn)的class(假設(shè)為class Widgetclass WidgetImpl);兩者的接口全部一致,而客戶代碼使用的是class Widget
    • class Widget中不對任何方法進(jìn)行具體實(shí)現(xiàn),只聲明類接口;且涉及到非基本類型的自定義類型成員變量(比如此處的class WidgetImpl),都使用前置聲明(智能)指針來進(jìn)行指涉;
    • 標(biāo)準(zhǔn)庫組件無需也不應(yīng)該被前置聲明;直接#include就行;
    • 這樣一來,class Widget的頭文件中不會#include任何其他的頭文件(除了標(biāo)準(zhǔn)庫);而這,也就杜絕了客戶代碼對除了class Widget頭文件之外的文件產(chǎn)生任何依賴
    • 至于class Widget的接口實(shí)現(xiàn),則在其.cpp文件中去#include "WidgetImpl",然后調(diào)用class WidgetImpl的接口即可;
  • interface class
    • 即類似于Java中的interface,不過實(shí)現(xiàn)方式是定義成虛基類;面向派生譜系的多態(tài)技術(shù);

條款33 避免遮掩繼承而來的名稱

  • C++應(yīng)對派生譜系中的函數(shù)調(diào)用,歸根結(jié)底就是以名稱為準(zhǔn)進(jìn)行匹配;
  • 無論是變量還是函數(shù),是重載還是重寫,是否是虛函數(shù),甚至也無論函數(shù)的參數(shù)列表是什么形式,都沒有任何關(guān)系;編譯器只要在當(dāng)前的域中找到了對應(yīng)的名稱,就直接結(jié)束匹配;
  • 這意味著:如果base class中定義了一組重載函數(shù),而后又在derived class中定義了一個(gè)同名的函數(shù),那么當(dāng)用derived class類型(或引用、指針)來調(diào)用這個(gè)名稱的函數(shù)時(shí),基類的重載函數(shù)統(tǒng)統(tǒng)被覆蓋;
  • 克服這個(gè)問題的方法:在派生類中加入using聲明:
class Base{
public:
    // 重載函數(shù)
    void f(int);
    void f();

};

class Derived : public Base {
public:
    using Base::f; // OK,基類的重載函數(shù)不會被覆蓋了
    void f(int, int);
};

  • 如何實(shí)現(xiàn)僅繼承部分基類接口?很簡單,使用private繼承+轉(zhuǎn)接函數(shù);
    • 所謂的轉(zhuǎn)接函數(shù)就是派生類中的公共接口,但這些公共接口只是去調(diào)用基類的函數(shù);
    • 基類因?yàn)楸籶rivate繼承了,所以其所有接口也就被隱藏了;

條款34 區(qū)分接口繼承和實(shí)現(xiàn)繼承

  • 在繼承譜系中,虛函數(shù),純虛函數(shù),普通函數(shù)之間的根本區(qū)別就是對待接口繼承實(shí)現(xiàn)繼承的方式不同;
  • 純虛函數(shù):只繼承接口;
  • 虛函數(shù):繼承接口和一份缺省實(shí)現(xiàn)
  • 普通函數(shù):繼承接口和一份強(qiáng)制實(shí)現(xiàn)======》
    • 這意味著任何derived class都不應(yīng)該重新定義base class中的普通函數(shù);
    • 條款36就是在陳述這一點(diǎn);本質(zhì)上就是因?yàn)槠胀ê瘮?shù)實(shí)施的是靜態(tài)綁定,相同的對象會因?yàn)槠渲羔樆蛞玫念愋偷牟煌鴪?zhí)行不同的函數(shù)體(有可能是基類的函數(shù)體,也可能是派生類的函數(shù)體);這造成了不確定性(另一方面,單個(gè)基類指針,即使指向不同類型的派生類,其調(diào)用普通函數(shù)時(shí),也只會執(zhí)行基類函數(shù)體,造成了程序錯誤);
  • 注:C++的虛函數(shù)模型在二進(jìn)制兼容性(ABI)方面的負(fù)面影響是極大的。如果一個(gè)程序會設(shè)計(jì)為一個(gè)動態(tài)庫,客戶代碼對其進(jìn)行加載調(diào)用,如果后續(xù)動態(tài)庫進(jìn)行了升級,在某個(gè)類中加入了新的虛函數(shù),那么如果客戶代碼不重新編譯的話,會直接調(diào)用不同的函數(shù),造成錯誤,因?yàn)榭蛻舸a在編譯結(jié)束以后,就直接以虛表指針加偏移的形式去調(diào)用函數(shù),而動態(tài)庫的各個(gè)函數(shù)的偏移可能在升級之后就完全改變了。

條款37 絕不重新定義繼承而來的缺省參數(shù)值

  • 雖然虛函數(shù)實(shí)行的是動態(tài)綁定,但虛函數(shù)(實(shí)際上是任何函數(shù))中的參數(shù)缺省值卻是靜態(tài)綁定的;
  • 這意味著函數(shù)的參數(shù)缺省值不應(yīng)該被重新定義;理由還是一樣的,這會因?yàn)橹羔樆蛞玫念愋筒煌斐刹淮_定性;
  • 如果需要為虛函數(shù)定義參數(shù)缺省值,則更好的做法是:
    • 定義一個(gè)普通函數(shù),有缺省值;
    • 實(shí)際的虛函數(shù)變?yōu)閜rivate,且無缺省值;
    • 使用普通函數(shù)去調(diào)用虛函數(shù);
    • 這樣就避免了代碼在派生譜系中的依賴性;

條款38 通過復(fù)合塑膜出has-a或“根據(jù)某物實(shí)現(xiàn)”

  • 關(guān)鍵就是理解復(fù)合(Composition)二字;復(fù)合包含應(yīng)用域和實(shí)現(xiàn)域兩種關(guān)系;
  • 應(yīng)用域:即把一個(gè)class作為組件;比如說class People的一個(gè)組件是class PhoneNumber;這就是所謂的has-a關(guān)系;
  • 實(shí)現(xiàn)域:即某個(gè)class需要通過另一個(gè)class進(jìn)行實(shí)現(xiàn),但兩者并不存在完美的繼承關(guān)系;比如說通過一個(gè)std::vector<int>來實(shí)現(xiàn)一個(gè)class Stack<int>;這就是所謂的Is-implemented-int-terms-of關(guān)系

條款39 明智而審慎地使用private繼承

  • private繼承并不具備“軟件設(shè)計(jì)”層面的意義,其僅僅是一種“軟件實(shí)現(xiàn)”的技術(shù);
  • 條款38中已經(jīng)闡述過"Is-implemented-in-terms-of"關(guān)系,事實(shí)上,private繼承也是這種意義;
  • “private繼承”和“復(fù)合”的區(qū)別就在于:
    • 一般情況下,能使用復(fù)合就使用復(fù)合;
    • 只有當(dāng)明確是Is-implemented-in-terms-of關(guān)系的同時(shí),需要重寫基類的虛函數(shù)或者訪問protect變量時(shí),才使用private繼承;因?yàn)檫@是復(fù)合無法做到的;
    • EBO(empty-base-optimization):C++中一個(gè)空類的size不等于0,而是1;而繼承一個(gè)空類不會加大size;這就是private的另一個(gè)優(yōu)勢;

條款40 明智而審慎地使用多重繼承

  • 總的來說,多重繼承還是有用的,但卻是也存在很多的限制;
  • 條款中所涉及的“虛繼承”概念是比較重要的:
    • 多重繼承很可能會發(fā)生所謂的菱形繼承:即某一個(gè)基類和某一個(gè)派生類之間存在多條繼承路徑;
    • 如果使用非虛繼承的話,派生類將會保存同一個(gè)基類的多個(gè)副本;但實(shí)際上一份副本就足夠了;這造成了空間浪費(fèi);更糟糕的則是因?yàn)槎喾莞北緦?dǎo)致的命名沖突;
    • 虛繼承是解決這個(gè)問題的唯一方法;它使得派生類可以只保留基類的一份副本;
    • 但虛繼承也有自己的缺點(diǎn):最突出的就是加大了運(yùn)行時(shí)消耗;因?yàn)椴扇√摾^承的話,class的size和內(nèi)存模型就只能在運(yùn)行期才能知曉了;(C++中虛函數(shù)、虛繼承內(nèi)存模型 - 知乎 (zhihu.com)

條款41 了解隱式接口和編譯器多態(tài)

  • 基于模板的泛型編程其實(shí)也隱含著“接口”的概念,但是是隱式的。這和派生譜系中的接口機(jī)制有很大不同;
  • 隱式接口是基于:必須滿足模板代碼中隱含的一組約束。比如書中給出的例子:if(w.size() > 10 && w != someNastyWidget){...},w的類型為typename T,那么就必須滿足:if中給出的表達(dá)式能夠轉(zhuǎn)換為bool類型。
  • 所謂的編譯器多態(tài)就是:編譯器根據(jù)隱式接口去決定需要(生成)調(diào)用哪一個(gè)重載函數(shù)以及具現(xiàn)化模板。

條款42 了解typename的雙重意義

  • 當(dāng)用于模板參數(shù)的時(shí)候,typaname和class沒有區(qū)別;
  • 如果某個(gè)名稱是嵌套從屬名稱(nested-dependent-names),即它的性質(zhì)(是變量名還是類型名)需要由模板參數(shù)來決定,那么如果它確實(shí)是一個(gè)類型名的話,就需要加上typename;(因?yàn)榫幾g器不知道它到底是什么東西);
  • 萃取器:即traits,通過模板以及模板偏特化技術(shù),將傳遞進(jìn)去的類型的一些相關(guān)特征給萃取出來。比如說typename std::iterator_traits<iteT>::value_type表示的就是iteT類型的迭代器所指涉的元素類型;萃取器的優(yōu)勢在于任何類型的迭代器(甚至是原生指針)都能萃取出想要的特征;

條款43 學(xué)習(xí)處理模板化基類的名稱

  • 模板化基類(templatized-base-class):也就是說繼承來的基類是一個(gè)模板,其具體是哪一個(gè)類暫時(shí)無法確定;
  • 當(dāng)模板化類繼承自一個(gè)模板化基類時(shí),編譯器就默認(rèn)基類中的所有名稱是無法得知的;除非顯式指出;
  • 編譯器之所以這樣做,是因?yàn)橛捎?strong>模板偏特化以及全特化的存在,使得模板化基類不一定會擁有模板中所寫的所用名稱;
  • 顯示指出的方法有3類:使用this->name;使用using BaseClass<T>::name;;顯式調(diào)用BaseClass<T>::name;其中,第3種方法會喪失動態(tài)綁定特性,因此不是很推薦;

條款44 將與參數(shù)無關(guān)的代碼抽離templates

  • 如果模板類中的某些函數(shù)與模板參數(shù)沒有關(guān)系,那么多個(gè)具現(xiàn)化的實(shí)體類則會擁有相同的函數(shù)體,這無疑使得目標(biāo)碼變得冗余;
  • 更好的做法是將這些與模板參數(shù)無關(guān)的代碼抽離出來,變成基類代碼或者其他,然后不同的模板的具現(xiàn)化class去共同調(diào)用這些相同的代碼(此時(shí)這些代碼就只有一份實(shí)體了);
  • 當(dāng)然,這樣也會存在一定問題。簡而言之,誰好誰壞,還是得由具體的運(yùn)行環(huán)境去決定;

條款45 運(yùn)用成員函數(shù)模板接受所有兼容類型

比如對于如下的一個(gè)模板類,很多時(shí)候,我們可能需要使用TmpDemo<int>去初始化一個(gè)tmpDemo<double>對象。這完全是合理的,但問題是,在模板編程的世界里,TmpDemo<int>TmpDemo<double>是完全沒有任何關(guān)系的?;蛘呖梢灾苯釉谀0孱愔卸x這樣一個(gè)構(gòu)造函數(shù),但如果遭遇了其他的需求呢?比如說int變?yōu)榱薱har,又或者,現(xiàn)在的typename是一個(gè)繼承譜系中的各種類型。顯然,單一的成員函數(shù)是解決不了問題的。

template <typename T>
class TmpDemo{
  // ...
};
  • 成員模板函數(shù)是解決這個(gè)問題的唯一方法;在成員函數(shù)中再聲明typename,來讓編譯器來處理各種需求;
  • 泛化構(gòu)造函數(shù)是成員模板函數(shù)的一種,它解決的是通過TmpDemo<U>來初始化TmpDemo<T>的問題;
  • 即使聲明了泛化構(gòu)造函數(shù),也還是要去自定義拷貝構(gòu)造函數(shù),這一點(diǎn)需要注意;

條款46 需要類型轉(zhuǎn)換時(shí)請為模板定義非成員函數(shù)

  • 該條款和條款24的思想是一致的,也就是當(dāng)函數(shù)的所有參數(shù)都涉及隱式轉(zhuǎn)換時(shí),它最好是一個(gè)非成員函數(shù)(因?yàn)閠his是無法轉(zhuǎn)換的);
  • 和條款24的不同之處在于,本條款涉及到的是模板類;即,某個(gè)函數(shù)的各個(gè)參數(shù)是模板類型;
  • 很顯然,這種函數(shù)也需要定義為非成員函數(shù);
  • 不同之處在于:因?yàn)樯婕暗搅四0?,那么在進(jìn)行函數(shù)模板的模板參數(shù)推導(dǎo)時(shí),絕對無法進(jìn)行隱式轉(zhuǎn)換,比如說對于如下的代碼,直接調(diào)用int ans = addFunc(tmp, 3);是無法通過編譯的,因?yàn)檫@涉及到了從3TmpDemo<T>(3)的隱式轉(zhuǎn)換;而這在函數(shù)模板參數(shù)推導(dǎo)中是絕對禁止的;
template <typename T>
class TmpDemo{
public:
  TmpDemo(const T& num){value = num;}
private:
  T num;
};

template <typename T>
const T addFunc(const TmpDemo<T> &t1, const TmpDemo<T> &t2){
  return t1.num * t2.num;
}

TmpDemo<int> tmp(2);
  • 解決方法就是把非成員函數(shù)定義在模板類的內(nèi)部,并聲明為friend。因?yàn)槟0孱悤ypename信息進(jìn)行硬編碼,就可以直接進(jìn)行轉(zhuǎn)換了。

條款49 了解new-handler的行為

  • new-handler:一個(gè)函數(shù)指針類型typedef void (*new_handler) ( );,并對應(yīng)一個(gè)global的函數(shù)指針,由用戶通過new_handler std::set_new_handler(new_handler p)填充其值(可能會有系統(tǒng)默認(rèn)值);當(dāng)new無法分配出足夠的空間時(shí),系統(tǒng)就會在拋出異常之前先調(diào)用這個(gè)函數(shù);
  • 通常情況下,擁有以下幾種行為的new-handler是更好的:
    • ①可以使得下一次調(diào)用new時(shí)有更大概率成功;這可以通過預(yù)先分配一塊大內(nèi)存,隨后每次調(diào)用new-handler時(shí)歸還部分內(nèi)存;
    • ②安裝其他new-handler和卸載本地的new-handler:各個(gè)class有可能會定義自己的new-handler,因此最好的做法是new不同的class的時(shí)候,調(diào)用各自不同的new-handler,并在調(diào)用完畢后將new-handler進(jìn)行恢復(fù);
    • ③拋出std::bad_alloc或者直接退出exit()std::abort()
  • 如何實(shí)現(xiàn)方式②?答:自定義operator new以及使用基于CRTP(curiously recursive template pattern)的模板技術(shù)
    • 為一個(gè)需要設(shè)置new-handler的class自定義一個(gè)operator new和set_new_handler,而在operator new內(nèi)部的流程就是:先調(diào)用std::set_new_handler設(shè)置自己的new-handler,隨后調(diào)用系統(tǒng)的new,再之后就是恢復(fù)new-handler到系統(tǒng)原本的值了;
    • 由于設(shè)置恢復(fù)完全適配于一個(gè)RAII,因此更優(yōu)秀的做法便是再設(shè)置一個(gè)資源管理類,在構(gòu)造函數(shù)內(nèi)保存之前的new-handler,并在析構(gòu)函數(shù)內(nèi)恢復(fù)之前的new-handler;
    • 接下來就是考慮這樣一個(gè)問題了,如果不同的class都需要自定義new-handler的話,而又由于自定義new-handler其實(shí)是一套完全一致的流程,除了各自的new-handler不一樣;因此CRTP就派上用場了,以下代碼就是完整的實(shí)例。
class HandleHolder{
public:
    HandleHolder(const HandleHoldr &) = delete; // 禁止拷貝
    HandleHolder &operator=(const HandleHolder &) = delete;

    HandleHolder(std::new_handler p): oldHandler(p) {}
    ~HandleHolder(){std::set_new_handler(oldHandler);}
private:
    std::new_handler oldHandler;
};

template <typename T>
class NewHandlerHelper{ // 此處沒有定義自己的set_new_handler了,感覺沒有必要
public:
    NewhandlerHelper(std::new_handler p): myHandler(p) {}
    static void *operator new(size_t size) throw(std::bad_alloc){ // 每個(gè)class對應(yīng)一個(gè)operator new
        HandleHolder tmp(std::set_new_handler(myHandler)); // std::set_new_handler會返回之前的new-handler
        return ::operator new(size);
        // tmp被析構(gòu),new-handler也就得以恢復(fù)
    }

private:
    static std::new_handler myHandler; // 每個(gè)class對應(yīng)一個(gè)new_handler
};

template <typename T>
std::new_handler NewHandlerHelper<T>::myHandler = nullptr; // static變量要記得初始化

class Widget : public NewHandlerHelper<Widget> { // 自己繼承自己,雖然看起來很奇怪,但實(shí)際上是行得通的;本質(zhì)上只是讓不同的class擁有不同的myHandler
    /**
     * ...
     * Widge只要在構(gòu)造函數(shù)處給NewHandlerHelper提供自己的new-handler即可
     * ...
     */
};

條款52 寫了placement new也要寫placement delete

  • 當(dāng)代碼中使用new表達(dá)式之后,發(fā)生了兩件事情:
    • ①調(diào)用void *operator new(size_t size)來獲取一塊原始內(nèi)存(raw memory);
    • ②調(diào)用class的ctor以構(gòu)造對應(yīng)的對象
  • 因?yàn)橛袃蓚€(gè)步驟的存在,因此,如果在第2個(gè)階段發(fā)生了異常,就有可能產(chǎn)生內(nèi)存泄漏;
  • 為了避免可能的內(nèi)存泄漏,當(dāng)發(fā)生上述情況時(shí),由系統(tǒng)來負(fù)責(zé)回收對應(yīng)的內(nèi)存;
  • 這就引出了一個(gè)問題,系統(tǒng)如何知道應(yīng)該調(diào)用哪一個(gè)版本的delete呢?系統(tǒng)的原則是,使用和operator new參數(shù)列表一致的operator delete
  • 因此就有了本條條款的原則:定義了一個(gè)placement new,就需要定義對應(yīng)的placement delete;所謂的placement就是參數(shù)列表除了size_t以外還包括其他的參數(shù);
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

  • 讓自己習(xí)慣C++ 條款01:視C++為一個(gè)語言聯(lián)邦 C++可視為: C:以C為基礎(chǔ)。 面向?qū)ο蟮腃++:添加面向?qū)?..
    Cloudox_閱讀 3,186評論 0 3
  • 這本書屬于“想提高必看之書”,相見恨晚,建議所有C++程序員都看看,沒事也可以拿出來翻翻。大家也可以瀏覽下面的筆記...
    拉普拉斯妖kk閱讀 771評論 0 1
  • 20190228暫停學(xué)習(xí),后續(xù)繼續(xù) 第一部分讓自己習(xí)慣C++ 條款1:視C++為一個(gè)語言聯(lián)邦 C++最初的名稱C ...
    去年匆匆今年匆匆閱讀 493評論 0 1
  • 第一章 從 C 到 C++ 條款1 C++是語言聯(lián)邦這條意思是C++支持面向過程、面向?qū)ο?、泛型編程、函?shù)編程、元...
    linanwx閱讀 354評論 0 0
  • 條款1:視C++為一個(gè)語言聯(lián)邦 1. C 用于C的基本特性。 2. 面向?qū)ο?封裝、繼承、多態(tài)、動態(tài)綁定、虛函數(shù)表...
    小張同學(xué)_loveZY閱讀 251評論 0 0

友情鏈接更多精彩內(nèi)容