EC43

43. 學習處理模板化基類內的名稱

  • 如果編譯期間有足夠信息確定類型,可以採用基於template解法。
    eg:如果要傳送消息給多個不同的公司,如果編譯期間有足夠的信息決定哪個信息傳至哪個公司,就可以採用基於template解法。

  • 模板全特化:
    看個例子:

    template<> //該語法象徵這既不是template,也不是標準class,而是特化版的MsgSender template,在template實參是CompanyZ時被使用
    class MsgSender<CompanyZ>
    {
    	public:
    	...
    }
    
  • 當一個類A繼承模板類B,並在A類中調用模板類的函數,此時編譯不會通過。【因爲編譯器不知道傳入的參數是不是上面所說的模板全特化,以至於不知道調用sendClear是不是合法的】
    eg:

    此時有三種方法避免:

    1)在基類函數調用動作之前加入“this->”。即在sendClear之前加上“this->”。【該方法告訴編譯器我繼承的是模板類的sendClear】

    2)使用using聲明式。即在定義sendClearMsg函數之前加個“using MsgSender<Company>::sendClear”【該方法告訴編譯器,讓他假設sendClear位於基類中】

    3)明白指出被調用的函數位於基類中。即用這種方式調用sendClear:MsgSender<Company>::sendClear(info);

總結:可以在繼承類的模板內通過this->指涉基類模板內的成員名稱,或者藉由一個明白寫出“基類資格修飾符”完成。究根結底,還是因爲編譯器解析繼承類模板定義式時發現指向基類的成員函數無效,所以要採取些行動讓編譯器知道是指向基類的成員函數還是指向繼承類或啥類的。

44.將與參數無關的代碼抽離templates

  • templates是節省時間和避免代碼重複的一個奇方妙法,雖然我現在還是對templates不太熟悉。
  • templates妙在讓編譯器具現化要用的函數。如class templates,其成員函數只有在被使用時才被暗中具現化,所有隻有在這幾個函數的每一個都被使用時纔會獲得這幾個函數,
  • 使用templates缺點:使用不當會導致代碼膨脹,雖然源碼看起來合身而整齊,但是目標碼卻膨脹。
  • “共性和變性分析”思想:當編寫某個class,若其中某個部分和其他類的某些部分相同,那麼就會將共同部分搬到新類中,然後用繼承或符合讓原先的類取用這共同特性;而原來互異部分仍然留在原位置不動。
  • 編寫templates時,也是做相同分析,以相同方式避免重複。但是在template代碼,重複是隱晦的,因爲只有一個template源碼,當其被具現化時纔會被發現是否重複了,這時就要自己感受了。(感覺還是挺抽象的)
  • 該條款只討論由非類型模板參數帶來的膨脹,即形如template<typenmae T, size_t n>,當傳入的T相同,而n不同時,這時候具現化就會產生代碼重複了。這時可以通過以函數參數或class成員變量替換非類型模板參數。

總結:從該條款get到的主要是是要將共同部分的代碼抽取出來,方便維護,以及避免代碼膨脹。

45. 運用成員函數模板接受所有兼容類型

  • STL容器的迭代器幾乎總是智能指針

  • 真實指針(即內置指針)一個很大的優點就是支持隱式轉換。eg:繼承類指針可以隱式轉換爲基類指針,指向non-const對象指針可以轉換爲指向const對象等。
    相應代碼例子:

    class Top{}
    class Middle: public Top{}
    class Bottom: public Middle{}
    
    Top *pt1 = new Middle; // 將Middle*轉換爲Top*
    Top *pt2 = new Bottom; // 將Bottom*轉換爲Top*
    const Top* pct2 = pt1; // 將Top*轉換爲const Top*
    
  • 構造模板是member function templates,其作用是爲class生成函數。其效用不限於構造函數,還支持賦值操作。

  • 泛化copy構造函數是什麼?【感覺就是模板裏面嵌套模板。。。。】

    template<typename T>
    class SmartPtr{
        public:
        // 該構造函數根據對象u創建對象t,因爲u和t是同一template的不同具現體,所以稱爲泛化copy構造函數
        // 爲了實現約束轉換行爲,如不能由基類轉換爲繼承類指針,於是模仿智能指針的get成員函數,返回內置類型
        template<typename U>
        SmartPtr(const SmartPtr<U>& other):heldPtr(other.get()){}
        T* get() const{return heldPtr;}
        
        private:
    	T* heldPtr; 
    }
    

    其目的就是可以支持隱式轉換。

  • 泛化copy構造函數(是個member function template)是不會改變語言規則,即它不會阻止編譯器生成它們自己的copy構造函數。所以如果我們想要控制copy構造函數,那麼我們要同時聲明泛化copy構造函數和“正常的”copy構造函數。同理,其亦使用於賦值操作。

總結:使用member function templates(成員函數模板)生成“可接受所有兼容類型”的函數。其實就是模板裏面嵌套另外一個模板,稱其爲泛化xxx函數。但若該模板用於泛化copy構造或泛化assignment操作,還是需要聲明正常的copy構造函數和copy assignment操作符。

46. 需要類型轉換時請爲模板定義非成員函數

  • template具現化的實參推導過程中,從不將**“通過構造函數而發生的”隱式類型轉換函數納入考慮**。解決方法:通過在template class內聲明爲friend函數。

  • 解釋該條款:爲了讓類型轉換可能發生在所有實參身上,所以聲明其爲非成員函數;爲了讓這個函數被自動具現化,所以要將其聲明在類內部;綜上,要將其變成friend函數。

  • 在一個class template內,template名稱可被用來作爲“template和其參數”的簡略表達方式,所以在Rational<T>內我們可以只寫Rational而不必寫Rational<T>

  • 聲明class template:(很多編譯器會強迫把所有template定義式放進頭文件內)

    template<typename T> class Rational;
    

總結:函數模板在實參推導過程中不能爲形參調用隱式轉換函數,而調用函數則可以使用隱式轉換函數。所以可以通過將其變爲friend函數轉換爲調用函數形式,從而執行隱式轉換函數。
eg:
不支持隱式轉換函數形式

template<typename T>
class Rational
{
    public:
    Rational(const T& numerator =0, const T& denominator = 1);
    ...
};

template<typename T> 
const Rational<T> operator*(const Rational<T>& lhs, const Rational<T>& rhs){}

支持隱式轉換函數形式

template<typename T>
class Rational
{
    public:
    ...
    friend 
        const Rational<T> operator*(const Rational<T>& lhs, const Rational<T>& rhs)
    {
        return Rational(lhs.xxx * rhs.xxx, lhs.xxx * rhs.xxx);
    }
};

47. 請使用trait classes變現類型信息

  • STL主要由“用以表現容器、迭代器和算法”的template構成,但也覆蓋若干工具性templates,其中一個名爲advance,用來將某個迭代器移動某個給定距離。

  • STL的5種迭代器分類:
    1)input迭代器:只能向前移動,一次一步,客戶只可讀取它們所指東西,只能讀取一次
    2)Output迭代器:只能向前移動,一次一步,客戶只可塗寫它們所指東西,只能塗寫一次
    3)forward迭代器:可以讀或寫所指物一次以上
    4)Bidirectional迭代器:除了可以向前移動,還可以向後移動。如list、set、multiset、map和multimap的迭代器

    5)random access迭代器:可以執行“迭代器算術”,可以在常量時間內向前或向後跳躍任意距離。如vector、deque和string提供的迭代器

  • traits是什麼?
    traits是一種技術,是一個C++程序員應該共同遵守的協議,其允許在編譯期間取得某些類型信息。並且要求其對內置類型和用戶自定義類型的表現必須一樣好。
    traits總是被實現爲structs,但它們卻又往往被稱爲traits classes

  • typedef:可以typedef用戶自定義類型,但是不可以定義指針。

  • iterator_traits爲指針指定的迭代器類型是:

    template<typename IterT>
    struct iterator_traits<IterT*>
    {
    	typedef random_access_iterator_tag iterator_category;
    }
    

總結:TR1導入許多新的traits classes用以提供類型消息;Traits classes使得“類型相關信息”在編譯期間可用,以templates和“templates特化”完成實現;

整合重載技術後,traits classes有可能在編譯期間對類型執行if…else測試。

48. 認識templates元編程

  • TMP(模板元編程)是執行於編譯期的過程,其是被發現而不是被髮明出來的。
  • TMP的兩個偉大效力:
    1)讓某些事情變得更容易
    2)將工作從運行期轉移到編譯期,使C++更高效:較少的可執行文件、較短的運行期、較少的內存需求;但編譯時間變長了
  • 運行期是運行到對應代碼才發現其是否有效,而編譯期是要確保所有源碼都有效,即使是不會執行的代碼。
  • TMP是個“圖靈完全”的機器,意思是它的威力大到足以計算任何事物。並且其是沒有真正的循環構件,所有循環效果是由遞歸完成,其主要是“函數式語言”。
  • 以三個例子論述TMP能達到的目標:
    1)使用TMP可以確保在編譯期程序中所有量度單位的組合正確,不論其計算多麼複雜。
    2)優化矩陣運算。
    3)可以生成客戶定製的設計模式。
    不理解這些例子也沒關係,反正只要知道他的最大特點就是:將工作從運行期轉移到編譯期,大大改善效率。

總結:TMP將工作有運行期轉移到編譯期,可以更早定位bug,實現早期錯誤偵測和更高的執行效率。就是有點過於抽象,不好調試+不好理解。

49. 瞭解new-handler的行爲

  • c++受程序員喜歡的原因之一是其允許手工管理內存,C++內存管理例程的主角是分配例程和歸還例程,配角是new-handler,這是當operator new無法滿足客戶的內存需求時所調用的函數。

  • heap是一個可被改動的全局性資源,因此涉及多線程時就要注意可改動的static數據。若沒有適當的同步控制,調用內存例程可能很容易導致管理堆的數據結構內容敗壞。

  • STL容器所使用的heap內存是由容器所擁有的分配器對象(allocator objects)管理,而不是直接被new和delete直接管理。

  • 當operator new拋出異常來反映一個未獲滿足的內存需求之前,它會先調用一個客戶指定的錯誤處理函數,即一個所謂的new-handler(處理new異常的方法)。客戶必須調用聲明在<new>的標準程序庫函數:set_new_handler指定這個“用以處理內存不足”的函數。

    namespace std
    {
        typedef void (*new_handler)();
        new_handler set_new_handler(new_handler p) throw();
    }
    
  • 如何使用set_new_handler?
    set_new_handler是以new_handler爲參數和返回值的,而new_handler是一個指向空參數和空返回值的函數指針,所以可以自己定義這樣的函數指針:

    void outOfMem()
    {
        std::cerr<<"Unable to satisfy request for memory\n";
        std::about();
    }
    
    int main()
    {
        std::set_new_handler(outOfMem);
        int *pBigDataArray = new int[111111111];
    }
    
  • 一個設計好的new-handler函數必須做的事情:
    1)讓更多內存可被使用。即程序一開始執行就分配一大塊內存,當new-handler第一次被調用,將他們釋放給程序使用。【所以是如果new過程有問題,那麼就把這大塊內存釋放掉就好】
    2)安裝另一個new-handler。若當前new-handler無法取得更多可用內存,可以調用新new-handler,令其修改“會影響new-handler行爲”的static數據、namespace數據或global數據。
    3)卸載new-handler。就是把null指針傳給set_new_handler,當沒有安裝任何new-handler,那麼operator new會在內存分配不成功時拋出異常。
    4)拋出bad_alloc(或派生自bad_alloc)的異常。該異常不會被operator new捕捉,因此會被傳播到內存索求處。
    5)不返回。通常調用abort或exit。

  • c++不支持class專屬的new-handler,也就是說,new-handler函數要是全局函數,即用static修飾。

  • nothrow形式:分配失敗便返回null行爲。
    [外鏈圖片轉存失敗,源站可能有防盜鏈機制,建議將圖片保存下來直接上傳(img-9Ta5UvJk-1569503305794)(C:\Users\panbaoru\AppData\Roaming\Typora\typora-user-images\1569381884697.png)]

    使用new nothrow創建的內存給widget,此時Widget得到了內存就可以執行它的構造函數,但是我們又不知道構造函數裏面是否有調用new,以及其是否是用new nothrow,導致構造函數裏面的new可能會導致異常,所以並沒有運用nothrow new的需要

總結:set_new_handler允許客戶指定一個函數(其格式是參數和返回值均爲void),在內存分配無法獲得滿足時被調用。nothrow new是個頗爲侷限的工具,因爲他只適用於內存分配,不能保證後繼的構造函數調用還是可能拋出異常。

50. new和delete的合理替換時機

  • 替換編譯器提供的operator new或operator delete的理由:
    1)用來檢測運用上的錯誤。
    2)爲了強化效能。自帶的new會導致破碎問題,導致程序無法滿足大區塊內存要求,即使有時總量足夠但分散爲許多小區塊的自由內存。
    3)爲了收集使用上的統計數據。因爲不知道軟件更傾向於使用FIFO還是LIFO形式分配和歸還內存。
    4)爲了增加分配和歸還的速度。
    5)爲了降低空間額外開銷
    6)爲了彌補非最佳齊位
    7)爲了將相關對象成簇集中
    8)爲了獲得非傳統的行爲,如分配和歸還共享內存。

總結

當遇到這些理由時候,可以考慮寫個自己的new和delete。

51. 編寫new和delete時要固守常規

  • 實現一致性operator new必須返回正確的值,內存不足時必須調用new-handling函數,必須有對付零內存需求的準備。
  • operator new的返回值:如果可以有能力供應客戶申請的內存,就返回一個指針指向那塊內存;若沒能力,就拋出bad_alloc異常。

總結

該條款教我們咋寫自己的new和delete。關於new,operator new應該內含一個無窮循環,並在其中嘗試分配內存,若無法滿足內存需求,則該調用new-handler,並且其要有能力處理0bytes申請。

關於delete,delete應該在收到null指針不做任何事情。

52. 寫了placement new也要寫placement delete

  • 如果operator new接受的參數除了一定會有的size_t之外還有其他,則成爲placement new。如:|

    void *operator new(std::size_t, void *pMemory) throw();
    
  • 如果運行期系統要取消operator new的分配並恢復舊觀,那麼運行期系統就會尋找“參數個數和類型都與operator new相同”的某個operator delete。【即new有什麼後面加的參數,相應的delete也要有相應的參數,這樣才配套】
    eg:

    class Widget{
        public:
        ...
        static void * operator new(size_t size, ostream& logStream) throw(bad_alloc);
        static void operator delete(void *pMemory) throw();
        static void operator delete(void *pMemory, ostream& logStream);
        
        // 在該例子中, 【ostream& logStream】 該參數就是新增的 所以delete也要有該參數
    }
    

    用例:

    Widget *pb = new (std::cerr) Base; // 可以省去size的參數輸入
    
  • 由於存在命名空間遮掩的情況,所以如果基類有寫placement new,那麼此時就不能用標準new方式new該類了,要避免這種情況,則需要建立一個基類,其內含所有正常形式的new和delete。

  • 要想以自定義形式擴充標準形式的客戶,可以利用繼承機制和using 聲明式。

總結

當我們寫一個placement operator new,那麼也要對應寫一個placement operator delete,以免出現無意造成的內存泄漏。並且還要考慮命名空間遮蔽情況,爲了要能使用標準new創建該頭像,要在基類含有正常形式的new和delete。

53. 不要輕忽編譯器的警告

emm現在編寫都是忽視warning,沒有error就萬事大吉了。。。。。不過編譯器的warning可以預測我們運行時結果和預期不符的bug,所以還是得瞅瞅warning,不改也起碼心裏有個底。

54-55. TR1和BOOST

TR1是一份規範,其實物是boost,兩個都包含很多東西,如TR1有智能指針、正則表達式、一般化函數指針等,boost還包括泛型編程,覆蓋一大組traits classes和模板元編程等。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章