(Effective C++)第七章 模板與泛型編程 (Templates and Generic Programming)

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

9.1 條款41:瞭解隱式接口和編譯期多態 (Understand implicit interface and compile-time polymorphism)

面向對象編程世界總是以顯示接口(explicit interfaces)和運行期多態(runtime polymorphism)解決問題。

class Widget{
public://函數顯示接口
Widget();
virtual ~Widget();
virtual std::size_t size()const;
virtual void normalize();
void swap(Widget &other);
};
void doProccessing(Widget &w)
{
    if (w.size() > 10 && w!= someNastWidget){
     Widget temp(w);
     temp.normalize();
     temp.swap(w);
}
}
//改爲模板
template <typename T>
void doProccessing(T &w)
{
    if (w.size() > 10 && w!= someNastWidget){
     T temp(w);
     temp.normalize();
     temp.swap(w);
}
}
示例9-1-1  模板
改成模板函數之前:
由於w的類型被聲明爲Widget,所以w必須支持Widget接口。
由於Widget的某些成員函數是virtual,w對那些函數的調用將表現出運行期多態(runtime polymorphism),也就是說將於運行期根據w的動態類型決定調用哪個函數。
改成模板函數之後:
Templates及泛型編程的世界,與面向對象有根本的不同,反倒是隱式接口(implicit interface)和編譯期多態(compile-time polymorphism)移到前面了。
W必須支持哪一種接口,系由template中執行於w身上的操作來決定。本例w的類型是T必須支持size,normalize和swap成員函數,copy構造函數。這一組表達式便是T必須支持的一組隱式接口(implicit interface)。
凡是涉及w的任何函數調用,例如operator>和operator!=,有可能造成template具現化(instantiated),使這些調用得以成功。“以不同的template參數具現化function templates”會導致調用不同的函數,這就是所謂的編譯期多態(compile-time polymorphism)。
運行期多態和編譯期多態之間的差異,類似於“哪個重載函數被調用”(發生在編譯期)和“哪個virtual函數被綁定”(發生在運行期)之間的差異。
通常顯示接口由函數的簽名式(也就是函數名稱,參數類型,返回類型)構成。
隱式接口並不基於函數簽名式,而是有效表達式(valid expression)組成。如本例doProccessing的template函數。
Classes和templates都支持接口(interface)和(polymorphism)。對於classes而言接口是顯式的,以函數簽名爲中心,多態則通過virtual函數發生在運行期。對於tempate參數而言,接口是隱式的,奠基與有效表達式。多態則是通過template具現化和函數重載解析(function overloading resolution)發生於編譯期。


9.2 條款42:瞭解typename的雙重意義 (Understand the two meaning of typename)

以下template聲明式中,class和typename有什麼不同?
template<class> class Widget;    //使用“class”
template<typename> class Widget;    //使用“typename”
答案:沒有不同。
假設我們需要打印容器的第二個元素,如下:

template <typename C>
void print2nd(const C &container)
{
    if (container.size() > 2){
     typename C::const_iterator iter(container.begin());
     ++iter;
int valude = *iter;
std::cout<<vaule;
}
}
示例9-2-1  print2nd模板
Template內出現的名稱如果相依 與某個template參數,稱爲從屬名稱(dependent names)。如果從屬名稱在class呈嵌套狀,稱爲嵌套從屬名稱(nested dependent names)。C::const_iterator就是這樣一個名稱。實際上,它還是一個嵌套從屬類型名稱(nested dependent type name),也就是個嵌套從屬名稱且指涉某類型。
Local變量value是一個並不依賴任何template參數的名稱,稱爲非從屬名稱(non-dependent names)。
嵌套從屬名稱可鞥導致解析困難,編譯器便假設這名稱不是一個類型,除非你告訴它是。所以缺省情況下,嵌套從屬名稱不是類型。
C::const_iterator被編譯器假設爲非類型,所以前面需要加關鍵字typename。

template <typename C>   //允許使用typename或class
void f(const C &container,  //不允許使用typename
typename C::iterator iter)  //一定要使用typename
{
}

template <typename T>   //允許使用typename或class
class Derived:public Base<T>::Nested{  //base class list中不允許typename
public:
explicit Derived(int)
:Base<T>::Nested(x)     //mem.init.list不允許使用typename
{
typename Base<T>::Nested temp; //一定要使用typename
}
}
//假設寫一個function template,它接收一個迭代器,而我們打算爲該迭代器指涉
//的對象做一份local復件temp,如下
template <typename IterT>   //允許使用typename或class
void workWithInterator(IterT iter)  
{
    typename std::iterator_traits<IterT>::vaule_type temp(*iter);
}
示例9-2-2  typename用法
使用typename關鍵字標識嵌套從屬類型名稱;但不得在base class lists(基類列)或member initialization list(成員初始列)內以它作爲base class修飾符。

9.3 條款43:學習處理模板化基類內的名稱 (Know how to access names in templatized base class)

假設寫一個程序,它能夠傳達信息到諾幹不同的公司去。信息可能是加密的譯文,可能是未經加工的文字。如果編譯期間,有足夠信息來決定哪一個信息傳至哪家公司,就可以採用基於template解法:

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);

};

class CompanyZ
{
public:
…        //這個class不提供sendCleartext
void sendEncrypted (const std::string &msg);
};
class MsgInfo {…};

template <typename Company>
class MsgSender
{
public:
void sendClear(const MsgInfo& info)
{
    std::string msg;
//根據info產生信息
Company c;
c.sendCleartext(msg);
}
}
Void sendSecret(const MsgInfo& info) //調用c.sendEncrypted
{…}
};
//後期需要記錄日誌
template <typename Company>
class LoggingMsgSender::public MsgSender<Company>
{
public:
void sendClearMsg(const MsgInfo& info)
{
    //將傳送前的信息寫至log
sendClear(info);
//將傳送後的信息寫至log
}
}
};
示例9-3-1  template解法
本例中,LoggingMsgSend調用base class函數sendClear將無法通過編譯。當編譯器遇到class template LoggingMsgSender定義式,並不知道它繼承什麼樣的class。雖然它繼承的是MsgSender<Company>,但是其中的Company並不知道是什麼,就無法調用sendClear函數。一般性的MsgSender template對於CompanyZ並不適合,因爲CompanyZ沒有sendCleartext函數。與糾正這個問題,我們針對CompanyZ產生一個MsgSender特化版本。
template <> //一個全特化的MsgSender,
class MsgSender<CompanyZ>{ //他和一般的template的差別在於它刪除了sendClear
public:

void sendSecret(const MsgInfo& info);
}
示例9-3-2  特化版
注意class定義式最前頭的“template <>”語法是個特化版本的MsgSender template,在template實參是CompanyZ時被使用。
我們再來看LoggingMsgSender:: sendClearMsg函數,如果Company==CompanyZ,sendclear不存在,編譯器往往拒絕在templatized base classes(模板化基類,本例的MsgSender<Company>)內尋找繼承而來的名稱。爲了解決該問題,有三個辦法:

//第一,在base class函數調用動作之前加上this->
template <typename Company>
class LoggingMsgSender::public MsgSender<Company>
{
public:
void sendClearMsg(const MsgInfo& info)
{
    //將傳送前的信息寫至log
this->sendClear(info);
//將傳送後的信息寫至log
}
}
};
//第二,使用using聲明書
template <typename Company>
class LoggingMsgSender::public MsgSender<Company>
{
public:
using MsgSender<Company>::sendClear;
void sendClearMsg(const MsgInfo& info)
{
    //將傳送前的信息寫至log
sendClear(info);
//將傳送後的信息寫至log
}
}
};
//第三,明白指出被調用的函數位於base class內,
//但是,不提倡,因爲它關閉virtual綁定行爲
template <typename Company>
class LoggingMsgSender::public MsgSender<Company>
{
public:
void sendClearMsg(const MsgInfo& info)
{
    //將傳送前的信息寫至log
MsgSender<Company>::sendClear(info);
//將傳送後的信息寫至log
}
}
};

示例9-3-3  三個方法

9.4 條款44:將與參數無關的代碼抽離templates (Factor parameter-independent code out of templates)

在non-template代碼中,重複十分明確:你可以“看”到兩個函數或兩個classes之間有所重複。然而在template代碼中重複是隱晦的:畢竟只存在一份template源碼,所以你必須訓練自己去感受當template被具現化多次時可能發生的重複。
假設你想爲固定尺寸的正方矩陣編寫一個template,該矩陣的性質之一是支持逆矩陣運算(matrix inversion)。

template <typename T,std::size_t n> //template支持n*n矩陣,
class SquareMatrix{
public:

void invert(); //求逆矩陣運算
};
SquareMatrix<double, 5> sm1;
sm1.invert();  //調用SquareMatrix<double, 5>::invert
SquareMatrix<double, 10> sm2;
sm2.invert();  //調用SquareMatrix<double, 10>::invert
示例9-4-1  正方矩陣求逆運算函數的與尺寸無關版本
本例會具現化兩份invert。這些函數並非完全相同,因爲其中一個操作是5*5矩陣而另一個操作的是10*10矩陣,但是除了常量5和10,兩個函數的其它部分完全相同。這會引出代碼膨脹。
下面是對SquareMatrix的第一次修改:
template <typename T>
class SquareMatrixBase{  //與尺寸無關的基類
protected: //避免derived classes代碼重複

void invert(std::size_t matrixSize); //以給定的尺寸求逆矩陣
};
template <typename T, std::size_t  n>
class SquareMatrix::private SquareMatrixBase<T>{
private:
using SquareMatrixBase<T>:: invert; //避免這樣base版的invert,條款33
public:

//製造inline,調用base版的invert
//不使用this->,就會使模板化基類內的函數名稱會被derived類掩蓋
void invert(){return this->invert(n);};
};
示例9-4-2  正方矩陣建立帶數值參數的函數
但是還有一個問題沒有解決,SquareMatrixBase<T>::invert如何知道哪個特定矩陣的數據在哪兒?一個方法是令SquareMatrixBase存儲一個指針,指向矩陣數值所在的內存。
template <typename T>
class SquareMatrixBase{  //與尺寸無關的基類
protected: //避免derived classes代碼重複
SquareMatrixBase(std::size_t n, T*pMem):size(n),pData(pMem){}

void invert(std::size_t matrixSize); //以給定的尺寸求逆矩陣
private:
std::size_t size;  //矩陣大小
T* pData;    //指針,指向矩陣數據
};
// derived classes之一
template <typename T, std::size_t  n>
class SquareMatrix::private SquareMatrixBase<T>{
private:
using SquareMatrixBase<T>:: invert; //避免這樣base版的invert,條款33
T data[n*n];
public:
SquareMatrix():SquareMatrixBase<T>(n,data){}

//製造inline,調用base版的invert
//不使用this->,就會使模板化基類內的函數名稱會被derived類掩蓋
void invert(){return this->invert(n);};
};
// derived classes之二
template <typename T, std::size_t  n>
class SquareMatrix::private SquareMatrixBase<T>{
private:
using SquareMatrixBase<T>:: invert; //避免這樣base版的invert,條款33
boost::scoped_array<T> pData;   //關於boost::scoped_array,條款13
public:
SquareMatrix():SquareMatrixBase<T>(n,0),  //將base class的數據指針設爲NULL
pData(new T[n*n])      //將指向矩陣內存的指針存儲起來
{ this->setDataPtr(pData.get());}  //將它的一個副本交給base class

//製造inline,調用base版的invert
//不使用this->,就會使模板化基類內的函數名稱會被derived類掩蓋
void invert(){return this->invert(n);};
};
示例9-4-3  正方矩陣求逆運算函數的與尺寸有關版本
此例硬是綁着矩陣尺寸的那個invert版本,有可能生成比共享版本更佳的代碼。
從另一個角度看,不同大小的矩陣只擁有單一版本的invert,可減少指向文件大小,也就是降低程序的working set大小,並強化指令高速緩存的引用集中化,提高了效率。
另一個關心的主題是對象大小。如果你不介意,可將前述“與矩陣大小無關的函數版本”搬至base class內,這會增加每一個對象的大小。
Template生成多個classes和多個函數,所以任何template代碼都不該與某個造成膨脹的template參數發生相依關係。
因非類型模板參數而造成的代碼膨脹可以消除,做法是以函數參數或class成員變量替換template參數。

因類型而造成的代碼膨脹,往往可以降低,做法是讓帶有完全相同二進制表述的具現類型共享實現嗎。例如,如果你實現某些成員函數而它們的操作強型指針(strongly typed pointers,T*),你應該令他們調用另一個操作無類型指針(untyped pointers,void*)。某些C++STL實現版本的確爲vector,deque和list等template做了這件事。


9.5 條款45:運用成員函數模板介紹所有兼容類型 (Use member function templates to accept "all compatible types")

所謂智能指針(Smart pointers)是“行爲像指針”的對象,並提供指針沒有的機能。真實指針做的很好的一件事,支持隱式轉換(implicit conversions)。
如果想在用戶自定的智能指針中模擬Derived class指針可以隱形轉換爲base class指針,或“指向non-const對象”的指針轉爲“指向const對象”等行爲,如下:

class Top {…};
class Middle:public Top {…};
class Bottom:public Middle {…};
Top* p1 = new Middle;
Top* p2 = new Bottom;
const Top* pc2 = pt1;

template <typename T>
class SmartPtr{  
public:
template<typename U>
SmartPtr(const SmartPtr<U> & other):heldPtr(other.get())
{…}; //member template copy構造函數
T* get const{return heldPtr;};

private:
T* heldPtr;  //這個SmartPtr持有的內置原始指針
};
SmartPtr<Top> pt1 = SmartPtr<Middle>(new Middle);
SmartPtr<Top> pt2 = SmartPtr<Middle>(new Bottom);
SmartPtr<const Top> pt2 = pt1;
示例9-5-1  泛化copy構造函數
此例,我們是爲了寫一個構造目標,這樣的模板是所謂member function templates,作用是爲class生成函數。對於任何類型T和任何類型U,這裏可以根據SmartPtr<U>生成一個SmartPtr<T>,因爲SmartPtr<T>有個構造函數接收一個SmartPtr<U>參數。
我們使用成員初始列來初始化SmartPtr<T>之內類型爲T*的成員變量。並以類型爲U*的指針作爲初值。這個行爲只有當“存在某個隱式轉換可將一個U*指針轉爲一個T*指針”時才能通過編譯。
Member function templates的效用不限於構造函數,他們常扮演的另一個角色是支持賦值操作。參見shared_ptr的實現。
在class內聲明泛化copy構造函數(一個template)並不會阻止編譯器生成他們自己的copy構造函數(一個non-template)。
Member function templates(成員函數模板)生成可以接受所用兼容類型的函數。
如果你聲明瞭Member templates用於泛化copy構造函數或泛化assignment操作,你還是需要聲明正常的copy構造函數和copy assignment操作符。

9.6 條款46:需要類型轉換時請爲目標定義非成員函數 (Define non-member functions inside templates when type conversions are desired)

條款24討論了爲什麼唯有non-member函數纔有能力“在所有實參身上實施隱式類型轉換”,該條款並以Rational class的operator*函數爲例。本條款將Rational和operator*模板化了:

template<typename T>
class Rational {  
public:
Rational (const T& numerator = 0, //條款20,pass by reference
const T& denominator = 1);
       const T numerator() const; //條款28,pass by vaule
       const T denominator() const; //條款3,返回const
};
template<typename T>
const Rational<T> operator*(const Rational<T> &lhs,
const Rational<T> &rhs);
{
return Rational(lhs.numerator()*rhs.numerator(),
lhs.denominator ()*rhs.denominator());
}
Rational<int> oneHalf(1,2);  //a =1/2
Rational<int> result;
  result = oneHalf * 2;         //錯誤無法通過編譯
示例9-6-1  Rational類的模板化
上述失敗的啓示是,模板化的Rational內的某些東西似乎和其non-template版本不同,這裏編譯器試圖具現化某個“名爲operator*並接受兩個Rational<T>參數”的函數,但是不知道T是什麼。原因在於,function template實參推到過程中從不將隱式類型轉換函數納入考慮,也就是沒有考慮通過構造函數而發生的隱式轉換。
Template class內的friend聲明式可以指涉某個特定函數,這就意味class Rational<T>可以聲明爲operator*是它的一個friend函數。Class template並不依賴function template實參推導,所以編譯器總能在class Rational<T>具現化時得知T。因此,令Rational<T> class聲明適當的operator*爲其friend函數:
template<typename T>
class Rational {  
public:
Rational (const T& numerator = 0, //條款20,pass by reference
const T& denominator = 1);
       const T numerator() const; //條款28,pass by vaule
       const T denominator() const; //條款3,返回const

friend const Rational operator*(const Rational &lhs,
const Rational &rhs);  //聲明

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

Rational<int> oneHalf(1,2);  //a =1/2
Rational<int> result;
  result = oneHalf * 2;         //通過編譯,但不能鏈接
示例9-6-2  Rational類聲明friend函數
在一個class template內,template名稱可被用來作爲“template和其參數”的簡略表達方式,所以在Rational<T>內我們可以只寫Rational而不必寫Rational<T>。
template<typename T>
class Rational {  
public:
Rational (const T& numerator = 0, //條款20,pass by reference
const T& denominator = 1);
       const T numerator() const; //條款28,pass by vaule
       const T denominator() const; //條款3,返回const

friend const Rational operator*(const Rational &lhs,
const Rational &rhs);
{
return Rational(lhs.numerator()*rhs.numerator(),
lhs.denominator ()*rhs.denominator());
}
};
template<typename T>
const Rational<T> operator*(const Rational<T> &lhs,
const Rational<T> &rhs);
{
return Rational(lhs.numerator()*rhs.numerator(),
lhs.denominator ()*rhs.denominator());
}

Rational<int> oneHalf(1,2);  //a =1/2
Rational<int> result;
  result = oneHalf * 2;         //通過編譯也能鏈接
示例9-6-3  Rational類定義friend函數
當我們編寫一個class template,而他所提供之“與此template相關的”函數支持“所有參數之隱式轉換”時,請講那些函數定義於“class template內部的friend函數”。

此時friend函數的用途不再是訪問class的non-public成分,而是讓類型轉換可能發生在所有實參身上。


9.7 條款47:請使用traits classes表現類型信息 (Use traits classes for information about types)

STL主要由“用以表現容器、迭代器和算法”的templates構成,但也覆蓋若干工具,如advance,用來將某個迭代器移動某個給定距離:
template<typename IterT, typename DistT>
void advance(IterT &iter, DistT d);  //如果d<0,則向後移動
對於STL的5種迭代器專屬的卷標接口(tag struct),如下:
struct input_iterator_tag{};
struct output_iterator_tag{};
struct forward_iterator_tag:public input_iterator_tag {};
struct bidirectinal_iterator_tag: public forward_iterator_tag {};
struct random_access_iterator_tag: public bidirectinal_iterator_tag {};
這些structs之間的繼承關係是有效的is-a關係。Advance的實現
template<typename IterT, typename DistT>
void advance(IterT &iter, DistT d);
{
    if (iter is random access iterator){
        iter+=d;
}
    else{
    if (d > =0) { while (d--) ++iter;}
    else { while (d++) --iter;}
}
}
示例9-7-1  Advance的實現
這種做法需要知道IterT是否爲random access迭代器分類。而Traits技術允許你在編譯期間取得某些類型信息。這個技術的要求之一,它對內置類型和用戶自定義類型的表現必須一樣好。比如,上述advance收到的實參是一個指針和一個int,advance仍然必須有效運作。但是它,排除了類型內的嵌套信息。因此,類型的traits信息必須位於類型自身之外。標準技術是把它放進一個template及其多個特化版本中。下面template用來處理迭代器分類的相關信息:
template<typename IterT >
struct  iterator_traits;
這裏的iterator_traits是個struct,又往往被稱爲traits classes。它的運作方式是,針對每一個類型IterT,在struct iterator_traits<IterT>內聲明某個typedef名爲iterator_category。這個typedef用確認IterT迭代器分類。例如:

template<…>
class deque {
public:
class iterator {
    typedef random_access_iterator_tag iterator_category;
};
};
template<…>
class list {
public:
class iterator {
    typedef bidirectinal_iterator_tag iterator_category;
};
};
template<typename IterT>
struct  iterator_traits {
    typedef  typename IterT::iterator_category iterator_category;
};
示例9-7-2 iterator_traits第一部分實現
此例iterator_traits對於用戶自定義類型行得通,但是對指針行不通,因爲指針不可能嵌套typedef。


template<typename IterT>
struct  iterator_traits< IterT *> {
    typedef  random_access_iterator_tag iterator_category;
};
示例9-7-2 iterator_traits第二部分實現
如何設計並實現traits class:
1)    確認若干你希望將來可取得的類型相關信息,例如對於迭代器而言,希望取得分類(category)。
2)    爲該信息選擇一個名稱,例如iterator_category。
3)    提供一個template和一組特化版本,內含你希望支持的類型相關信息。
好了,我們對advice實踐先前的爲嘛:
template<typename IterT, typename DistT>
void advance(IterT &iter, DistT d);
{
    if (typeid(typname std::iterator_traits<IterT>::iterator_category)
== typeid(std::random_access_iterator_tag)){
}
。。。
}
IterT類型在編譯期間獲知,所以std::iterator_traits<IterT>::iterator_category也可在編譯期間確定。
   把條件式改成重載:
template<typename IterT, typename DistT>
void doAdvance(IterT &iter, DistT d, std::random_access_iterator_tag);
{
   iter +=d;
}
void doAdvance(IterT &iter, DistT d, std::bidirectinal_iterator_tag);
{
 if (d > =0) { while (d--) ++iter;}
else { while (d++) --iter;}
}
void doAdvance(IterT &iter, DistT d, std::input_iterator_tag);
{
 if (d < 0) {thow std::out_of_range(“Negative distance”);}
else { while (d++) --iter;}
}
template<typename IterT, typename DistT>
void advance(IterT &iter, DistT d);
{
    doAdvance(
iter, d,   // 對iter之迭代器分類而言
typename  //必須的
std::Iterator_traits<IterT>::iterator_category());
}
示例9-7-3  Advance的再次實現
總結如何使用一個traits class了:
1)    建立一組重載函數(身份像勞工)或函數模板(例如doAdvance),彼此之間差異在於各自的traits參數。
2)    建立一個控制函數(身份像工頭)或函數模板(例如advance),它調用哪些勞工函數並傳遞traits class所提供的信息。

9.8 條款48:認識template元編程 (Be aware of template metaprogramming)

Template metaProgramming(TMP,模板元編程)是編寫template-based C++程序並執行於編譯期的過程。所謂template metaprogram(模板元程序)是以C++寫成、執行於C++編譯器內的程序。
Template metaProgramming(TMP,模板元編程)可將工作由運行期移往編譯期,因而得以實現早期錯誤偵查和更高的效率。
TMP可被用來生成“基於政策選擇組合”(base on combination of policy choices)的客戶定製代碼,也可用來避免生成對某些特殊類型並不適合的代碼。
其他見《Effective C++ 中文 第三版》P233。


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