Effective C++ (7): Templates and Generic Programming

Introduction

這一章主要介紹了模板編程, 最核心的思想就是儘可能地把運行期所做的事情移動至編譯期完成. 最後還簡要介紹了以下 TMP

Rule 41: Understand implicit interfaces and compile-time polymorphism

瞭解隱式編程和編譯期多態.
對於着一條 Rule, 需要瞭解到的就是 template 的多態與 classes 的多態有所不同, 一個是發生與編譯期, 一個是發生於運行期. 就效率而言 template 更快. 但是 template 要保持接口的一致是隱式的(implicit), 就是說如果在寫一個 template 函數中, 如果使用了 w.size() 這樣的函數, 實際上也就隱式要求了這個 type 需要有size這個接口函數. 所以也增加了寫代碼的難度.

Remeber:

  • classes 和 templates 都支持接口(interfaces) 和 多態(polymorphism)
  • 對 classes 而言接口是顯式的(explicit), 以函數簽名爲中心. 多態則是通過 virtual 函數發生在運行期
  • 對 template 參數而言, 接口是隱式(implicit)的, 基於有效表達式, 多態則是通過 template 具體化和函數重載解析發生於編譯期

Rule 42: Understand the two meanings of typename

看一端代碼就好:

template<typename/class C>  //template 和 class 是一樣的
void print2nd(const C& container)
{
    typename C::const_iterator iter(container.begin()); //對於嵌套從屬名稱, 需要在前面加上typename
}

Remeber:

  • 聲明 template 參數的時候, class = typename
  • 使用關鍵字 typename 表示嵌套從屬類型名稱, 但是不能在 base class lists(基類列) 或 member initialization list(成員初值列) 內以它作爲 base class 修飾符

Rule 43: Know how to access names in templatized base classes

當繼承 base class templates 時候, 需要通過 ‘this->’ 來調用 base class templates 中的成員. 例如:

template<typename Company>
class MsgSender{
public:
    ...
    void sendClear(const MsgInfo& info)
    {
        std::string msg;
        Company c;
        c.sendCleartext(msg);
    }
    void sendSecret(const MsgInfo& info)
    { ... }
};

template<typename Company>
class LoggingMsgSender: public MsgSender<Company>
{
public:
    void sendClearMsg(const MsgInfo &info)
    {
        ...
        sendClear(info);  // 無法通過編譯
        this->sendClear(info);  // 通過編譯
        ...
    }
};

這是因爲編譯器無法確定其中的Company是否有 sendClear 這個函數(考慮到特化的問題). 所以需要通過加上 “this->” 來向編譯器承諾 base class template 的任何特化版本都將支持其一般(泛化)版本所提供的接口

除了加上 “this->” 之外還有另外兩種方法:

  1. 使用 using 聲明式, 例如 using MsgSender::sendClear
  2. 使用明確資格修飾符, 例如 MsgSender::sendClear(info). 不建議使用這種方法, 因爲會關閉 ” virtual 綁定行爲 “

Remeber:
當繼承 template class 的時候, 需要通過 “this->” 來調用 base class templates 內的成員名稱, 或者通過寫出 “base class 資格修飾符” 完成

Rule 44: Factor parameter-independent code out of templates

使用template可能會導致代碼膨脹的問題. 特別要注意對於 非類型參數(non-type parameter), 例如:

template<typename T, std::size_t n>
class SquareMatrix{
public:
    ...
    void invert();
};

上面的 size_t n 就是非類型參數, 當 n 取 5, 10 的時候, 就會產生兩份代碼, 除了常量5和10不同, 其他都相同, 這樣就容易造成代碼膨脹.
解決這種問題最好的方法就是, 將操縱的代碼放在一個 base template class 中, 如果存在數據的問題就在 base template class 中保存一個指向數據的指針, 然後真正的數據放在 derived template class 中, derived class 接受非類型參數, derived template class 中除了存放數據, 其他的函數就直接調用 base 中的函數, 這樣一來重複的代碼只會有 derived class 中的代碼.
例如:

template<typename T>        // 減少代碼膨脹
class SquareMatrixBase{
protected:
    SquareMatrixBase(std::size_t n, T* pMem):
        size(n), pData(pMem) {}
    void setDataPtr(T* ptr) { pData = ptr; }
    void invert(std::size_t matrixSize);
    ...
private:
    std::size_t size;
    T* pData;   // 存儲數據指針
};

template<typename T, std::size_t n>
class SquareMatrix: private SquareMatrixBase<T>{
public:
    SquareMatrix() : SquareMatrixBase<T>(n, data) {}
    ...
private:
    T data[n*n];
};

除了這種由 non-type template parameters 之外, type template parameters 也會引起膨脹. 例如含有指針的時候 list<const int*>list<const double*>, 最好使用 void* 來代替.

Remeber:

  • Templates會根據參數生成多份代碼, 容易造成代碼冗餘
  • 因非類型模板參數(non-type template parameters)而造成的代碼膨脹, 可通過 以函數參數或者 class成員變量替換template參數.
  • 因類型參數(type parameters)而造成的代碼膨脹, 可以讓帶有完全相同二進制表述(binary representation)的表現類型共享實現代碼

Rule 45: Use member function templates to accept “all compatible types”

運用成員函數模板接收所有兼容類型

舉個例子, 希望能夠實現:

template<class T>
class shared_ptr{
public:
    template<class Y>
        explicit shared_ptr(Y* p);  // 只可以顯示轉換
    template<class Y>
        shared_ptr(shared_ptr<Y> const& r); // 允許隱式轉換
    ...
};

這個就是在模板類中定義泛化模板函數.

Remeber:

  • 請使用 member function templates(成員函數模板) 生成 “可接收所有兼容類型” 的函數
  • 如果你聲明瞭 member templates 用於”泛化 copy 構造” 或 “泛化 assignment 操作”, 你還是需要聲明正常的 copy 構造函數和 copy assignment 操作符

Rule 46: Define non-member functions inside templates when type conversions are desired

之前有提到過, 如果對於 “所有參數之類型轉換” 的函數, 就把函數作爲 non-member function, 再提供隱式類型轉換.
不過對於 template class 卻有不同. 需要將函數定義爲 friend 函數, 並且就在類內部實現

template<typename T>
class Ration{
public:
    ...
    friend const Rational operator*(const Rational& lhs, const Rational& rhs)    // 聲明爲 friend 函數
    {
        return Rational(lhs.numerator() * rhs.numerator(),
                    lhs.denominator() * rhs.denominator());
    }   // 在類內部實現
};

Remeber:
當編寫一個 class template 的時候, 他所提供的函數支持 “所有參數之隱式類型轉換” 時, 請將那些函數定義爲 “class template 內部的friend函數”

Rule 47: Use traits classes for information and types

首先, 以 iterator 的5種不同類型作爲引入:

  1. input_iterator: 單向, 每次只能操作一次
  2. output_iterator: 同上
  3. forward_iterator: 可以做之前兩種迭代器的任意一種, 並且可以操作多次
  4. bidirectional_iterator: 可以雙向操作
  5. random_access_iteratror: 最強大, 以常量時間隨意跳躍

好的, 下面介紹 Traints classes + 函數重載來解決下面這個問題: 我們需要寫一個讓 iterator 進行跳轉的函數, 根據不同的 iterator 類型有不同的實現:

template<typename IterT, typename DistT>
void advance(IterT &iter, DistT d);

那麼我們要解決兩個問題: 1. 如何判斷 IterT 是哪種類型的 iterator. 2. 如果做到在編譯器根據類型的不同有不同的實現

對於這種情況, 我們採用了 Traits classes 來讓 iterator 夾帶類型信息. 簡而言之, 就是定義一個 iterator_traits, 並用它來傳遞類型信息, 並且在各個 iterator 自身定義 tag 信息, 而 iterator_traits 就是把這個 tag 信息傳遞出去

template<typename IterT>
struct iterator_traits{     // IterT 的 iterator_category 其實就是用來表現 "IterT自己來說自己是啥", 所以說 iterator_traits 有個傳遞的作用
    typedef typename IterT::iterator_category iterator_category;  // typedef typename 用來告訴編譯器是個別名, 不是類型
    ...
};

/* 對於指針迭代器提供一個偏特化版本 */
template<typename IterT>
struct iterator_traits<IterT*>{ 
    typedef random_access_iterator_tag iterator_category;
    ...
};

template <...>
class deque{
public:
    class iterator{
    public:
        typedef random_access_iterator_tag iterator_category; // 定義自己的類型, random_access_iterator_tag 是一個 struct
        ...
    };
    ...
};

/* 通過建立一個重載函數 doAdvance 接受traits信息,  和一個控制函數 advance來傳遞traits信息 */
template<typename IterT, typaname DistT>
void advance(IterT &iter, DistT d)
{
    doAdvance(iter, d, 
        typename std::iterator_traits<IterT>::iterator_category());
}

template<typename IterT, typaname DistT>
void doAdvance(IterT &iter, DistT d,
                    std::random_access_iterator_tag)
{
    iter += d; 
}

template<typename IterT, typaname DistT>
void doAdvance(IterT &iter, DistT d,
                    std::bidirectional_iterator_tag)
{
    if ( d >= 0) { while (d--) ++iter; }
    else { while(d++) --iter; }
}

Remeber:

  • Traits classes 使得”類型相關信息”在編譯期可用. 他們以 templates 和 “templates 特化” 完成實現
  • 整合重載技術(overloading)後, traints classes 有可能在編譯期對類型執行 if…else 測試

Rule 48: Be aware of template metaprogramming

模板元編程(TMP) 被證明是圖靈完備的, 之前 Rule 47就是TMP的一個案例, 由於TMP還是比較麻煩並且不直觀的, 所以這裏簡要介紹一個 TMP 的 Hello world, 求 factorial:

template<unsigned n>
struct Factorial{
    enum { value = n * Factorial<n-1>::value };
};

template<>          //特化版本
struct Factorial<0>{
    enum { value = 1 };
};

int main(){
    std::cout<<< Factorial<10>::value ;
}

Remeber:
Template metaprogramming(TMP, 模板元編程) 可將工作由運行期移往編譯期, 因而得以實現早期錯誤偵測和更高的執行效率

系列文章

Effective C++ (1): Accustoming Yourself to C++
Effective C++ (2): Constructors, Destructors, and Assignment Operators
Effective C++ (3): Resource Management
Effective C++ (4): Designs and Declaration
Effective C++ (5): Implementation
Effective C++ (6): Inheritance and Oject-Oritent Design
Effective C++ (7): Templates and Generic Programming
Effective C++ (8): Customizing new and delete
Effective C++ (9): Miscellany

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