1. 程式人生 > >Effective C++: 07模板與泛型編程

Effective C++: 07模板與泛型編程

單向 不可 允許 non-const 內容 卷標 基類 complete ear

C++ template機制自身是一部完整的圖靈機(Turing-complete):它可以被用來計算任何可計算的值。於是導出了模板元編程(TMP, template metaprogramming),創造出“在C++編譯器內執行並於編譯完成時停止執行”的程序。

41:了解隱式接口和編譯期多態

所謂顯式接口(explicit interface),是指在源碼中明確可見的接口,顯式接口由函數的簽名式(也就是函數名稱、參數類型、返回類型)構成。而運行時多態,就是指繼承和virtual帶來的動態綁定機制。

在Templates及泛型編程的世界裏,隱式接口和編譯期多態更重要一些。比如下面的模板定義:

template<typename T>
void doProcessing(T& w)
{
  if (w.size() > 10 && w != someNastyWidget) {
     T temp(w);
     temp.normalize();
     temp.swap(w);
  }
}

w必須支持哪一種接口,是由template中執行於w身上的操作來決定。本例看來w的類型T好像必須支持size,normalize和swap成員函數、copy構造函數、不等比較。這一組表達式便是T必須支持的一組隱式接口(implicit interface)。

凡涉及w的任何函數調用,例如operator>和operator!=,有可能造成template具現化(instantiated),使這些調用得以成功。這樣的具現行為發生在編譯期。“以不同的template參數具現化function templates”會導致調用不同的函數,這便是所謂的編譯期多態。

42:了解typename的雙重意義

以下template聲明式中,class和typename的意義是完全相同的,但更推薦typename:

template<class T> class Widget;                 //
uses "class" template<typename T> class Widget; // uses "typename"

但是,某種情況下必須使用typename。首先看下面的函數:

template<typename C>  
void print2nd(const C& container) 
{                                               // this is not valid C++!
  if (container.size() >= 2) {
     C::const_iterator iter(container.begin()); // get iterator to 1st element
     ++iter;                                    // move iter to 2nd element
     int value = *iter;                         // copy that element to an int
     std::cout << value;                        // print the int
  }
}

在上面的函數模板中,iter的類型是C::const_iterator,而實際是什麽必須取決於template參數C。如果 template內出現的名稱相依於某個template參數,稱之為從屬名稱(dependent names)。如果從屬名稱在class內呈嵌套狀,我們稱它為嵌套從屬名稱(nested dependent name )。C::const_iterator就是這樣一個名稱。實際上它還是個嵌套從屬類型名稱(nested dendent type name ),也就是個嵌套從屬名稱並且指涉某類型。

另一個local變量value,其類型是int。int是一個並不倚賴任何template參數的名稱。這樣的名稱是謂非從屬名稱(non-dependent names)。

嵌套從屬名稱有可能導致解析困難。比如:

template<typename C>
void print2nd(const C& container)
{
  C::const_iterator * x;
  ...
}

看上去好像是我們將 x 聲明為一個指向 C::const_iterator 的局部變量。但編譯器不這麽認為,比如如果 C 有一個靜態數據成員碰巧就叫做 const_iterator 呢?而且 x 碰巧是一個全局變量的名字呢?在這種情況下,上面的代碼就不是聲明一個 局部變量,而成為 C::const_iterator 乘以 x!

直到 C 成為已知之前,沒有任何辦法知道 C::const_iterator 到底是不是一個類型。C++ 有一條規則解決這個歧義:如果解析器在一個模板中遇到一個嵌套從屬名字,它假定那個名字不是一個類型,除非你明確告訴它。所以,一開始的print2nd中的語句並不合法:

C::const_iterator iter(container.begin());   

iter 的 聲明僅在 C::const_iterator 是一個類型時才有意義,但是我們沒有告訴 C++ 它是,所以C++ 就假定它不是。要想糾正這個錯誤,必須明確指出C::const_iterator 是一個類型:這就必須使用typename:

template<typename C>                           // this is valid C++
void print2nd(const C& container)
{
  if (container.size() >= 2) {
    typename C::const_iterator iter(container.begin());
    ...
  }
}

一般性規則很簡單:任何時候當你想要在template中指涉一個嵌套從屬類型名稱,就必須在緊臨它的前一個位置放上關鍵字typename。

typename只被用來驗明嵌套從屬類型名稱,其他名稱不該有它存在。例如:

template<typename C>                   // typename allowed (as is "class")
void f(const C& container,             // typename not allowed
     typename C::iterator iter);       // typename required

上述的C並不是嵌套從屬類型名稱(它並非嵌套於任何“取決於template參數”的東西內),所以聲明container時並不需要以typename為前導,但C::iterator是個嵌套從屬類型名稱,所以必須以typename為前導。

上面的規則有個例外:typename不可以出現在base classes list內的嵌套從屬類型名稱之前,也不可在成員初始化列表中作為base class修飾符。例如:

template<typename T>
class Derived: public Base<T>::Nested { // base class list: typename not allowed
public:                                 
  explicit Derived(int x)
  : Base<T>::Nested(x)          // base class identifier in mem
  {                                // init. list: typename not allowed

    typename Base<T>::Nested temp;      // use of nested dependent type
    ...                                 // name not in a base class list or
  }                                     // as a base class identifier in a
  ...                                   // mem. init. list: typename required
};

來看最後一個例子:

template<typename IterT>
void workWithIterator(IterT iter)
{
  typename std::iterator_traits<IterT>::value_type temp(*iter);
  ...
}

上面的函數以叠代器為參數,函數第一條語句的意思是為該叠代器所指向的對象創建一個副本。std::iterator_traits<IterT>::value_type 就是表示IterT所指對象的類型。比如如果 IterT 是 vector<int>::iterator,則temp 就是 int 類型。

std::iterator_traits<IterT>::value_type是一個嵌套從屬類型名稱(value_type 嵌套在 iterator_traits<IterT>內部,而且 IterT 是一個 模板參數),所以必須在它之前放置 typename。

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

看一下下面的代碼,它表示需要將信息發到若幹不同的公司,信息要麽是明文,要麽是密文:

class CompanyA {
public:
  ...
  void sendCleartext(const std::string& msg);
  void sendEncrypted(const std::string& msg);
  ...
};

class CompanyB {
public:
  ...
  void sendCleartext(const std::string& msg);
  void sendEncrypted(const std::string& msg);
  ...
};
...                                     // classes for other companies

class MsgInfo { ... };                  // class for holding information
                                        // used to create a message
template<typename Company>
class MsgSender {
public:
  ...                                   // ctors, dtor, etc.
  void sendClear(const MsgInfo& info)
  {
    std::string msg;
    create msg from info;

    Company c;
    c.sendCleartext(msg);
  }

  void sendSecret(const MsgInfo& info)   // similar to sendClear, except
  { ... }                                // calls c.sendEncrypted
};

現在假設有了新的需求,需要在每次發送明文是記錄日誌。此時可以通過繼承實現:

template<typename Company>
class LoggingMsgSender: public MsgSender<Company> {
public:
  ...                                    // ctors, dtor, etc.
  void sendClearMsg(const MsgInfo& info)
  {
    //write "before sending" info to the log;

    sendClear(info);                     // call base class function;
                                         // this code will not compile!
    //write "after sending" info to the log;
  }
  ...
};

上面的代碼無法通過編譯,編譯器會抱怨sendClear不存在。盡管在base class中確實定義了sendClear。

問題在於,當編譯器見到LoggingMsgSender這個模板類的定義時,並不知道它繼承什麽樣的類。當然它繼承的是MsgSender<Company>,但其中的Company是個template參數,只有在具現化LoggingMsgSender才知道Company是什麽,而如果不知道Company是什麽,就無法知道MsgSender<Company>是否有個sendClear函數。

比如,現在有個公司只能發送密文:

class CompanyZ {                             // this class offers no
public:                                      // sendCleartext function
  ...
  void sendEncrypted(const std::string& msg);
  ...
};

此時一般性的MsgSender對CompanyZ就不合適了,必須產生一個MsgSender特化版:

template<>                                 // a total specialization of
class MsgSender<CompanyZ> {                // MsgSender; the same as the
public:                                    // general template, except
  ...                                      // sendCleartext is omitted
  void sendSecret(const MsgInfo& info)
  { ... }
};

現在有個MsgSender針對CompanyZ的全特化版本,再次考慮LoggingMsgSender的實現,當base class指定為MsgSender<CompanyZ>時,這段代碼不合法,因為該base class沒有sendClear函數。

這就是編譯失敗的原因,編譯器知道base class模板類可能被特化,而特化版本可能不提供一般性模板相同的接口,所以它才拒絕在模板化基類(本例中的MsgSender<Company>)內尋找繼承而來的名稱(本例中的SendClear)。

有三種辦法可以明確指出使編譯器進入模板基類中尋找名稱:第一是在base class的函數調用時加上this->:

template<typename Company>
class LoggingMsgSender: public MsgSender<Company> {
public:
  ...
  void sendClearMsg(const MsgInfo& info)
  {
    //write "before sending" info to the log;
    this->sendClear(info);    // okay, assumes that sendClear will be inherited
    //write "after sending" info to the log;
  }
  ...
};

第二是使用using聲明式:

template<typename Company>
class LoggingMsgSender: public MsgSender<Company> {
public:
  ...   
  using MsgSender<Company>::sendClear;   // tell compilers to assume
                                 // that sendClear is in the base class                                  
  void sendClearMsg(const MsgInfo& info)
  {
    ...
    sendClear(info);   // okay, assumes that sendClear will be inherited
    ...   
  }
};

第三是明確指出函數位於base class內:

template<typename Company>
class LoggingMsgSender: public MsgSender<Company> {
public:
  ...
  void sendClearMsg(const MsgInfo& info)
  {
    ...
    MsgSender<Company>::sendClear(info);      // okay, assumes that
    ...                                       // sendClear will be inherited
  }                                           
};

第三種方法有個缺點,就是當被調用的virtual函數時,會關閉virtual綁定行為。

從名字可見性的觀點來看,上面方法都做了同樣的事情:它向編譯器保證任何後繼的 base class template(基類模板)的特化版本都將支持通用模板提供的接口。

但是如果保證被證實不成立,真相將在後繼的編譯過程中暴露。例如,如果後面的源代碼中包含這些:

LoggingMsgSender<CompanyZ> zMsgSender;
MsgInfo msgData;
...                                          // put info in msgData
zMsgSender.sendClearMsg(msgData);            // error! won‘t compile

對 sendClearMsg 的調用將不能編譯,因為在此刻,編譯器知道 base class是MsgSender<CompanyZ>,也知道那個類沒有提供 sendClear函數。

從根本上說,問題就是編譯器是早些(當 derived class template definitions(派生類模板定義)被解析的時候)診斷對 base class members(基類成員)的非法引用,還是晚些時候(當那些 templates(模板)被特定的 template arguments(模板參數)實例化的時候)再進行。C++ 的方針是寧願早診斷,而這就是為什麽當那些 classes(類)被從 templates(模板)實例化的時候,它假裝不知道 base classes(基類)的內容。

44:將與參數無關的代碼抽離template

為了避免重復代碼,當編寫某個普通函數,其中某些部分的實現碼和另一個函數的實現碼實質相同,此時,抽出兩個函數的共同部分,把它們放進第三個函數中,然後令原先兩個函數調用這個新函數。同樣道理,如果你正在編寫某個class,而其中某些部分和另一個class的某些部分相同,可以把共同部分搬移到新class去,然後使用繼承或復合,令原先的classes取用這共同特性。而原classes的互異部分仍然留在原位置不動。

編寫templates時,也可以做同樣的優化,以相同的方式避免重復。在non-template代碼中,重復十分明確;然而在template代碼中,重復是隱晦的。

舉個例子,為固定尺寸的正方矩陣編寫一個支持逆矩陣運算的template:

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

SquareMatrix<double, 5> sm1;
sm1.invert();                  // call SquareMatrix<double, 5>::invert

SquareMatrix<double, 10> sm2;
sm2.invert();                  // call SquareMatrix<double, 10>::invert

sm1.invert和sm2.invert函數調用會具現化兩份invert。這些函數並非完完全全相同,但除了常量5和10,兩個函數的其他部分完全相同。這是template引出代碼膨脹的一個典型例子。

首先想到為它們建立一個帶數值參數的函數,然後以5和10來調用這個帶參數的函數,而不重復代碼:

template<typename T>                   // size-independent base class for
class SquareMatrixBase {               // square matrices
protected:
  ...
  void invert(std::size_t matrixSize); // invert matrix of the given size
  ...
};

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

SquareMatrixBase也是個template,不同的是它只對“矩陣元素對象的類型”參數化,不對矩陣的尺寸參數化。因此對於某給定的元素對象類型,所有矩陣共享同一個SquareMatrixBase class。它們也將因此共享這唯一一個class內的invert函數。

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

智能指針的行為像指針,並提供真實指針沒有的機制保證資源自動回收。真實指針支持隱式轉換:Derived class指針可以隱式轉換為base class指針,指向non-const對象的指針可以轉換為指向const對象的指針等,比如:

class Top { ... };
class Middle: public Top { ... };
class Bottom: public Middle { ... };
Top *pt1 = new Middle;                   // convert Middle* => Top*
Top *pt2 = new Bottom;                   // convert Bottom* => Top*
const Top *pct2 = pt1;                   // convert Top* => const Top*

如果想在自定義的智能指針中模擬上述轉換:

template<typename T>
class SmartPtr {
public:                             // smart pointers are typically
  explicit SmartPtr(T *realPtr);    // initialized by built-in pointers
  ...
};

SmartPtr<Top> pt1 =                 // convert SmartPtr<Middle> =>
SmartPtr<Middle>(new Middle);       // SmartPtr<Top>

SmartPtr<Top> pt2 =                 // convert SmartPtr<Bottom> =>
SmartPtr<Bottom>(new Bottom);       // SmartPtr<Top>

SmartPtr<const Top> pct2 = pt1;     // convert SmartPtr<Top> =>
                                    // SmartPtr<const Top>

上面的代碼是錯誤的。同一個template的不同具現體之間並不存在什麽先天的固有關系,也就是說:如果以帶有base-derived關系的S, D兩類型分別具現化某個template,產生出來的兩個具現體並不帶有base-derived關系。

所以,為了獲得SmartPtr classes之間的轉換能力,必須明確寫出來。上面的語句時創建智能指針對象,所以考慮構造函數的實現。但是不可能寫出所有需要的構造函數,上面的代碼中,根據一個SmartPtr<Middle> 或 SmartPtr<Bottom> 構造出一個 SmartPtr<Top>,但是如果將來這個繼承體系被擴充,還需要重新定義一個構造函數,這是不現實的。

我們需要的不是為SmartPtr寫一個構造函數,而是寫一個構造模板,這就是所謂的member function templates:

template<typename T>
class SmartPtr {
public:
  template<typename U>                       // member template
  SmartPtr(const SmartPtr<U>& other);        // for a "generalized
  ...                                        // copy constructor"
};

這個構造模板的意思是:對任何類型T和任何類型U,這裏可以根據SmartPtr<U>生成一個SmartPtr<T>,有時又稱之為泛化copy構造函數。

上面的聲明還不夠:我們希望根據一個SmartPtr<Bottom> 創建一個 SmartPtr<Top>,但是不需要能夠從一個 SmartPtr<Top> 創建一個 SmartPtr<Bottom>,因此必須在某方面對這個member template所創建的成員函數群進行篩除。可以這樣做:

template<typename T>
class SmartPtr {
public:
  template<typename U>
  SmartPtr(const SmartPtr<U>& other)         // initialize this held ptr
  : heldPtr(other.get()) { ... }             // with other‘s held ptr

  T* get() const { return heldPtr; }
  ...
private:                                     // built-in pointer held
  T *heldPtr;                                // by the SmartPtr
};

這裏使用U*初始化T*,這個行為只有在:存在某個隱式轉換可將U*指針轉換為T*指針時才能通過編譯。

成員函數模板的作用並不僅限於構造函數,它還可以作用於賦值操作副。

如果類沒有定義copy構造函數,編譯器會自動生成一個。在類內聲明泛化copy構造函數並不會阻止編譯器生成它們自己的copy構造函數。這個規則也適用於賦值操作。

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

條款24討論過為什麽只有非成員函數才能“在所有實參身上實施隱式類型轉換”,該條款以Rational的operator*函數為例。現在將Rational和operator*模板化,代碼如下:

template<typename T>
class Rational {
public:
  Rational(const T& numerator = 0,     // see Item 20 for why params
           const T& denominator = 1);  // are now passed by reference

  const T numerator() const;           // see Item 28 for why return
  const T denominator() const;         // values are still passed by value,
  ...                                  // Item 3 for why they‘re const
};

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

像條款24一樣,我們希望支持混合式算術運算,所以我們希望下面的代碼通過編譯:

Rational<int> oneHalf(1, 2);
Rational<int> result = oneHalf * 2;   // error! won‘t compile

上面的代碼並不能通過編譯。在條款24中,編譯器知道我們嘗試調用什麽函數,但是這裏編譯器卻不知道。雖然這裏有個函數模板是operator*,它接受兩個Rational<T>參數,但是在模板實參推導過程中,從不考慮隱式類型轉換。

這裏的調用中,operator*的參數一個是Rational<int>,另一個是int類型,沒有這樣的模板函數可以具現化出這樣的函數,所以編譯失敗。

可以利用以下規則解決這個問題:模板類中的friend聲明可以指涉某個特定函數:

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

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

此時,當對象oneHalf被聲明為一個Rational<int>時,類Rational<int>也就被具現化出來了,那麽friend函數operator*也就自動被聲明出來。後者作為一個函數而不是函數模板,編譯器可以再調用它時使用隱式轉換。

此時,代碼雖然能夠通過編譯,但是卻無法鏈接成功。當聲明一個Rational<int>時,該類被具現化出來,但其中的friend聲明也僅僅是個聲明,還沒有找到定義,也就是函數體並未具現化(盡管在Rational外部提供了該friend的定義,但是那是一個函數模板,在沒有遇到參數匹配的函數調用之前,不會具現化,這裏的調用時oneHalf*2,參數不匹配,所以不會具現化這個模板)。

最簡單的解決辦法就是講定義體放在Rational內:

template<typename T>
class Rational {
public:
  ...

friend const Rational operator*(const Rational& lhs, const Rational& rhs)
{
  return Rational(lhs.numerator() * rhs.numerator(),       // same impl
                  lhs.denominator() * rhs.denominator());  // as in
}                                                          // Item 24
};

現在可以通過編譯、連接,並能執行了。

這個技術的趣味點是:雖然使用了friend,但是與friend的傳統用途“訪問class中非public部分”毫不相幹:為了讓類型轉換可以作用於所有實參上,需要一個non-member函數(條款24);為了這個函數能自動具現化,需要將它聲明在class內部;而在class內部聲明non-member函數的唯一辦法就是令他成為一個friend。

條款30中說,class內定義的函數都自動成為inline,包括像operator*這樣的friend函數。可以這樣將inline聲明所帶來的沖擊最小化:讓operator*不做任何事情,而是調用一個定義與class外部的輔助函數(本例中意義不大,因為operator*已經是個單行函數,但對復雜函數而言,這樣做也許有意義)。

Rational是個模板,意味著那個輔助函數通常也是個模板,所以代碼如下:

template<typename T> class Rational; 

template<typename T>                                    
const Rational<T> doMultiply(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)   // Have friend
  { return doMultiply(lhs, rhs); }                      // call helper
  ...
};

template<typename T>                                      // define
const Rational<T> doMultiply(const Rational<T>& lhs,      // helper
                             const Rational<T>& rhs)      // template in
{                                                         // header file,
  return Rational<T>(lhs.numerator() * rhs.numerator(),   // if necessary
                     lhs.denominator() * rhs.denominator());
}

因為定義在Rational內部的operator*需要調用doMultiply函數模板,所以,需要在Rational之前聲明doMultiply,而doMultiply原型中,又用到了Rational模板,所以在它之前又需要聲明Rational。

作為一個template,doMultiply當然不支持混合式乘法,但它其實不需要。他只是被operator*調用,而operator*支持混合式乘法,也就是調用operator*時,參數已經完成了隱式轉換。

47:使用traits classes表現類型信息

STL中有一個名為advance的template,它用於將某個叠代器移動指定距離:

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

只有隨機訪問叠代器支持+=操作,所以其他類型的叠代器,在advance中只能反復執行++或--操作,供d次。

STL共有5中叠代器分類,對應於它們支持的操作:input叠代器只能向前移動,一次一步,只能讀取它們所指的東西,而且只能讀取一次,istream_iterators就是這種叠代器;output叠代器類似,只能向前移動,一次一步,只能寫它們所指的東西,且只能寫一次,ostream_iterators是這類叠代器;forward叠代器,可以做前述兩種類型叠代器所能做的每件事,且可以讀或寫所指物一次以上,單向鏈表類型的容器的叠代器就屬於forward叠代器;bidirectional叠代器除了可以向前移動,也可以向後移動。set,map等的叠代器屬於這一類;最強大的叠代器是random access叠代器,它更強大的地方在於可以執行叠代器算術,也就是常量時間內向前或向後跳躍任意距離。vector、deque,string的叠代器屬於這一類,指針也被當做random access叠代器。

對於這5中叠代器,C++標準庫提供了卷標結構用以區分:

struct input_iterator_tag {};
struct output_iterator_tag {};
struct forward_iterator_tag: public input_iterator_tag {};
struct bidirectional_iterator_tag: public forward_iterator_tag {};
struct random_access_iterator_tag: public bidirectional_iterator_tag {};

回到advance函數,它的偽代碼應該是下面這個樣子:

template<typename IterT, typename DistT>
void advance(IterT& iter, DistT d)
{
  if (iter is a random access iterator) {
     iter += d;                                      // use iterator arithmetic
  }                                                  // for random access iters
  else {
    if (d >= 0) { while (d--) ++iter; }              // use iterative calls to
    else { while (d++) --iter; }                     // ++ or -- for other
  }                                                  // iterator categories
}

這就需要判斷iter是否為random access叠代器,也就是我們需要取得類型信息。這就是traits的作用,它允許你在編譯期獲得某些類型信息。traits是一種技術,也是一種約定,它的要求之一是需要對內置類型和用戶自定義類型要表現的一樣好。也就是說,advance收到的實參如果是一個指針,則advance也需要能夠運作。

因為traits需要支持內置類型,所以traits信息必須位於類型自身之外。比如,在標準庫中,針對叠代器的traits就命名為iterator_traits:

template<typename IterT> struct iterator_traits; 

習慣上,traits總是被實現為structs。iterator_traits的運作方式是,針對每一個類型IterT,在struct iterator_traits<IterT>內一定聲明某個typedef名為iterator_category。這個typedef用來確認IterT的叠代器分類。

iterator_traits以兩部分實現上述所言。首先它要求每一個用戶自定義的叠代器類型必須嵌套一個typedef,名為iterator_category。比如deque和list的叠代器定義:

template < ... > 
class deque {
public:
  class iterator {
  public:
    typedef random_access_iterator_tag iterator_category;
    ...
  };
  ...
};

template < ... >
class list {
public:
  class iterator {
  public:
    typedef bidirectional_iterator_tag iterator_category;
    ...
  };
  ...
};

而在iterator_traits這個模板類中,只是簡單的鸚鵡學舌:

// the iterator_category for type IterT is whatever IterT says it is;
template<typename IterT>
struct iterator_traits {
  typedef typename IterT::iterator_category iterator_category;
  ...
};

上面的做法對於指針是行不通的,所以有一個偏特化版本:

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

現在可以對advance實踐先前的偽代碼:

template<typename IterT, typename DistT>
void advance(IterT& iter, DistT d)
{
  if (typeid(typename std::iterator_traits<IterT>::iterator_category) ==
     typeid(std::random_access_iterator_tag))
  ...
}

實際上這個實現是有編譯問題的,而且IterT類型在編譯期間就可知了,所以iterator_traits<IterT>::iterator_category也可以在編譯期間確定。但if語句卻是在運行期才能確定。

我們真正需要的是在編譯期間就能判斷類型是否相同,在C++中,函數的重載就是在編譯期間確定類型的例子。所以,可以使用重載技術實現advance:

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

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

template<typename IterT, typename DistT>             
void doAdvance(IterT& iter, DistT d, std::input_iterator_tag)
{
  if (d < 0 ) {
     throw std::out_of_range("Negative distance");    
  }
  while (d--) ++iter;
}

forward_iterator_tag 繼承自 input_iterator_tag,所以上面的doAdvance的input_iterator_tag版本也能處理forward叠代器。

實現了這些doAdvance重載版本之後,advance的代碼如下:

template<typename IterT, typename DistT>
void advance(IterT& iter, DistT d)
{
  doAdvance( 
    iter, d, 
    typename std::iterator_traits<IterT>::iterator_category() 
  );             
} 

TR1導入了許多新的traits classes用以提供類型信息,包括is_fundamental<T>判斷T是否為內置類型,is_array<T>判斷T是否是數組,以及is_base_of<T1, T2>判斷T1和T2是否相同,或者是否T1是T2的base class。

48:認識template元編程

所謂template metaprogram(TMP,模板元程序)是以C++寫成、執行於C++編譯期內的程序。一旦TMP程序結束執行,其輸出,也就是從templates具現出來的若幹C++源碼,便會一如往常地被編譯。

TMP有兩個效力。第一,它讓某些事情更容易。如果沒有它,那些事情將是困難的,甚至不可能的。第二,由於template metaprograms執行於C++編譯期,因此可將工作從運行期轉移到編譯期。這導致的一個結果是,某些錯誤原本通常在運行期才能偵測到,現在可在編譯期找出來。另一個結果是,使用TMP的C++程序可能在每一方面都更高效:較小的可執行文件、較短的運行期、較少的內存需求。然而將工作從運行期移轉至編譯期的另一個結果是,編譯時間變長了。

條款47中提到的advance的traits實現,就是TMP的例子。在條款47中,還提到了一種運行期判斷類型的實現:

template<typename IterT, typename DistT>
void advance(IterT& iter, DistT d)
{
  if (typeid(typename std::iterator_traits<IterT>::iterator_category) ==
      typeid(std::random_access_iterator_tag)) {
     iter += d;                                     // use iterator arithmetic
  }                                                 // for random access iters
  else {
    if (d >= 0) { while (d--) ++iter; }             // use iterative calls to
    else { while (d++) --iter; }                    // ++ or -- for other
  }                                                 // iterator categories
}

這個實現不但效率低,而且還會存在編譯問題,比如針對std::list<int>::iterator iter的具現化代碼如下:

void advance(std::list<int>::iterator& iter, int d)
{
  if (typeid(std::iterator_traits<std::list<int>::iterator>::iterator_category) ==
      typeid(std::random_access_iterator_tag)) {
    iter += d;                                        // error!
  }
  else {
    if (d >= 0) { while (d--) ++iter; }
    else { while (d++) --iter; }
  }
}

問題出在iter+=d上,這條語句嘗試在list<int>::iterator上使用其不支持的+=操作。盡管在運行期間永遠不會執行+=操作,但是編譯期間編譯器必須確保所有源碼有效,某條語句執行類型不支持某種操作肯定會引起編譯錯誤。

TMP已被證明是個圖靈完備的,也就是說TMP可以計算任何事物,使用TMP可以聲明變量,執行循環,編寫調用函數等。比如循環,TMP中沒有真正的循環構件,循環效果是通過遞歸實現的。以計算階乘為例:

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

template<>                           // special case: the value of
struct Factorial<0> {                // Factorial<0> is 1
  enum { value = 1 };
};

可以這樣使用Factorial:

int main()
{
  std::cout << Factorial<5>::value;            // prints 120
  std::cout << Factorial<10>::value;           // prints 3628800
}

以上只是一個TMP最簡單的例子,TMP還有很多更酷的玩法。

TMP的缺點是語法不夠直觀,或許TMP不會成為主流,但是對某些程序員,特別是程序庫開發人員,幾乎肯定會成為他們的主要糧食。

Effective C++: 07模板與泛型編程