跳至內容

模板 (C++)

本頁使用了標題或全文手工轉換
維基百科,自由的百科全書

這是本頁的一個歷史版本,由Newmangling留言 | 貢獻2014年9月25日 (四) 03:52編輯。這可能和目前版本存在著巨大的差異。

模板Template)指C++程式設計語言中的函式模板類別模板[1]大體對應於javaC#中的泛型。目前,模板已經成為C++的泛型編程中不可缺少的一部分。

模板定義以關鍵字template開始,後接模板形參表(template parameter list),模板形參表是用尖括號括住的一個或者多個模板形參的列表,形參之間以逗號分隔。模板形參可以是表示類型的類型形參,也可以是表示常數表達式的非類型形參。非類型形參跟在類型說明符之後聲明。類型形參跟在關鍵字class或typename之後聲明。

使用模板時,可以在模板名字後面顯式給出用尖括號括住的模板實參列表(template argument list)。對模板函式或類的模板成員函式,也可不顯式給出模板實參,而是由編譯器根據函式呼叫的上下文推導出模板實參,這稱為模板參數推導(template argument deduction)。

模板是C++程式員絕佳的武器, 特別是結合了多重繼承(multiple inheritance)與運算子重載(operator overloading)之後。C++ 的標準函式庫提供許多有用的函式大多結合了模板的觀念,如STL以及IO Stream

簡介

函式模板

以下以取最大值的函式模板maximum為例。此函式在編譯時會自動產生對應輸入變數的資料類別的實際程式碼。

#include <iostream>

template <typename T>
inline const T& maximum(const T& x,const T& y)
{
   if(y > x)
      return y;
   else
      return x;
}

int main(void)
{
   using namespace std;
   //Calling template function
   std::cout << maximum<int>(3,7) << std::endl;         //输出 7
   std::cout << maximum(3, 7) << std::endl;             //和上面相同
   std::cout << maximum<double>(3.0,7.0) << std::endl;  //输出 7
   return 0;
}

類別模板

以下以將元件指標的操作,封裝成類別模板ComPtr為例。
#pragma once

template <typename Ty>
class ComPtr
{
protected:
    Ty* m_ptr;
    
public:

    ComPtr()
    {
        m_ptr = NULL;
    }

    ComPtr(const ComPtr& rhs)
    {
        m_ptr = NULL;
        SetComPtr(rhs.m_ptr);
    }

    ComPtr(Ty* p)
    {
        m_ptr = NULL;
        SetComPtr(p);
    }

    ~ComPtr()
    {
        Release();
    }

    const ComPtr& operator=(const ComPtr& rhs)
    {
        SetComPtr(rhs.m_ptr);
        return *this;
    }

    Ty* operator=(Ty* p)
    {
        SetComPtr(p);
        return p;
    }

    operator Ty* ()
    {
        return m_ptr;
    }

    Ty* operator->()
    {
        return m_ptr;
    }

    operator Ty** ()
    {
        Release();
        return &m_ptr;
    }

    operator void** ()
    {
        Release();
        return (void**)&m_ptr;
    }

    bool IsEmpty()
    {
        return (m_ptr == NULL);
    }

    void SetComPtr(Ty* p)
    {
        Release();
        
        m_ptr = p;
        if (m_ptr)
        {
            m_ptr->AddRef();
        }
    }

    void Release()
    {
        if (m_ptr)
        {
            m_ptr->Release();
            m_ptr = NULL;
        }
    }
};

模板的巢狀:成員模板

對於類中的模板成員函式、巢狀的成員類別模板, 可以在封閉類的內部或外部定義它們。當模板成員函式、巢狀類別模板在其封閉類的外部定義時,必須以封閉類別模板的模板參數(如果它們也是模板類)和成員模板的模板參數開頭。[1]如下例:

template <typename C> class myc{
  public:
    template <typename S> C foo(S s);
};

//下行需要给出外部类与内部嵌套类的模板形参列表:
template<typename C> template <typename S> C myc<C>::foo(S s){
C var;
return var;   
}

int main()
{
float f;
myc<int> v1;
v1.foo(f);
}

C++標準規定:如果外圍的類別模板沒有特例化,裡面的成員模板就不能特例化( "the declaration shall not explicitly specialize a class member template if its enclosing class templates are not explicitly specialized as well")。例如:

template <class T1> class A {
  template<class T2> class B {
      template<class T3> void mf1(T3);
      void mf2();
  };
};

template <> template <class X>
   class A<int>::B {
      template <class T> void mf1(T);
   };

template <> template <> template<class T>
    void A<int>::B<double>::mf1(T t) { }

template <class Y> template <>
     void A<Y>::B<double>::mf2() { } // ill-formed; B<double> is specialized but its enclosing class template A is not

template與typename關鍵字

template關鍵字有兩個用途:

  1. 常見的在模板定義的開始。
  2. 是模板類內部定義了模板成員函式或者巢狀的成員模板類。當參照這樣的模板成員函式或巢狀的成員模板類時,可以在::(作用域解析)運算子、.(以對象方式訪問成員)運算子、->(以指標方式訪問成員)運算子之後使用template關鍵字,隨後才是模板成員函式名字或巢狀的成員模板類名字,這使得隨後的左尖括號<被解釋為模板參數列的開始,而不是小於號運算子。例如:
template <class T> class A{
    template <class U> class B{
         public: typedef int INT;
     };
     public:   template <class V> void foo(){};
};
int main()
{
  typename A<float>::template B<double>::INT i;
  i=101;
  A<bool> a, *p=&a;
  a.template foo<char>();
  p->template foo<long>();
  return 0;
}

typename關鍵字有兩個用途:

  1. 常見的在模板定義中的模板形參列表,表示一個模板參數是類型參數。等同於使用class
  2. 使用模板類內定義的類型的名字時,顯式指明這個名字是一個類型名。否則,這個名字會被理解為模板類的靜態成員名。

模板實例化

模板實例化(template instantiation )是指在編譯或連結時生成函式模板或類別模板的具體實例原始碼。ISO C++定義了兩種模板實例化方法:隱式實例化(當使用實例化的模板時自動地在當前代碼單元之前插入模板的實例化代碼)、顯式實例化(直接聲明模板實例化)。在C++語言的不同實現中,模板編譯模式(模板初始化的方法)大致可分為三種:

  • Borland模型(包含模板編譯模式):編譯器生成每個編譯單元中遇到的所有的模板實例,並存放在相應的目的檔中;連結器合併相同的模板實例,生成可執行檔。為了在每次模板實例化時模板的定義都是可見的,模板的聲明與定義放在同一個.h檔案中。這種方法的優點是連結器只需要處理目的檔;這種方法的缺點是由於模板實例被重複編譯,編譯時間被加長了,而且不能使用系統的連結器,需重新設計連結器
  • Cfront/查詢模型(分離(Separation)模板編譯模式):AT&T公司C++編譯器Cfront為解決模板實例化問題,增加了一個模板倉庫,用以存放模板實例的代碼並可被自動維護。當生成一個目的檔時,編譯器把遇到的模板定義與當前可生成的模板實例存放到模板倉庫中。連結時,連結器的包裝程式(wrapper)首先呼叫編譯器生成所有需要的且不在模板倉庫中的模板實例。這種方法的優點是編譯速度得到了最佳化,而且可以直接使用系統的連結器;這種方法的缺點是複雜度大大增加,更容易出錯。使用這種模型的源程式通常把模板聲明與非行內的模板成員分別放在.h檔案與模板定義檔案中,後者單獨編譯
  • 混合(迭代)模型:g++目前是基於Borland模型完成模板實例化。g++未來將實現混合模型的模板實例化,即編譯器編譯單元中的模板定義與遇到的當前可實現的模板實例存放在相應的目的檔中;連結器的包裝程式(wrapper)呼叫編譯器生成所需的目前還沒有實例化的模板實例;連結器合併所有相同的模板實例。使用這種模型的源程式通常把模板聲明與非行內的模板成員分別放在.h檔案與模板定義檔案中,後者單獨編譯

ISO C++標準規定,如果隱式實例化模板,則模板的成員函式一直到參照時才被實例化;如果顯式實例化模板,則模板所有成員立即都被實例化,所以模板的聲明與定義在此處都應該是可見的,而且在其它程式文字檔案使用了這個模板實例時用編譯器選項抑制模板隱式實例化,或者模板的定義部分是不可見的,或者使用template<> type FUN_NAME(type list)的語句聲明模板的特化但不實例化。

g++的模板實例化,目前分為三種方式:

  • 不指定任何特殊的編譯器參數:按Borland模型寫的原始碼能正常完成模板實例化,但每個編譯單元將包含所有它用到的模板實例,導致在大的程式中無法接受的代碼冗餘。需要用GNU連結器刪除各個目的檔中冗餘的模板實例,不能使用作業系統提供的連結器
  • 使用-fno-implicit-templates編譯選項:在生成目的檔時完全禁止隱式的模板實例化,所有模板實例都顯式的寫出來,可以存放在一個單獨的原始檔中;也可以存放在各個模板定義檔案中。如果一個很大的原始檔中使用了各個模板實例,這個原始檔不用-fno-implicit-templates選項編譯,就可以自動隱式的生成所需要的模板實例。在生成庫檔案時這個編譯選項特別有用。
  • 使用-frepo編譯選項:在生成每個目的檔時,把需要用到的當前可生成的模板實例存放在相應的.rpo檔案中。連結器包裝程式(wrapper)—collect2將刪除.rpo檔案中冗餘的模板實例並且修改相應的.rpo檔案,使得編譯器可以利用.rpo檔案知道在那裡正確放置、參照模板實例,並重新編譯生成受影響的目的檔。由作業系統的通用的連結器生成可執行檔。這對Borland模型是很好的模板實例化方法。對於使用Cfront模型的軟體,需要修改原始碼,在模板標頭檔的末尾加上#include <tmethods.cc>。不過MinGW中不包含連結器包裝程式collect2,故不使用此方法。對於庫(library),建議使用顯式實例化方法。
  • 另外,g++擴充了ISO C++標準,用extern關鍵字指出模板實例在其它編譯單元中顯式聲明;用inline關鍵字實例化編譯器支援的資料(如虛表)但不實例化模板成員;用static關鍵字實例化模板的靜態資料成員但不實例化其它非靜態的模板成員。
  • g++在4.1版本仍然不支援模板實例化的export關鍵字

VC++7.0中必須類別模板實例化只有Borland模型;函式模板一般隱式實例化,自5.0版以後也可顯式實例化。

參考文獻

  1. ^ 1.0 1.1 MSDN:巢狀的類別模板