跳至內容

模板 (C++)

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

模板Template)在C++程序設計語言中,是指函數模板與模板[1],是一種參數化類型機制。JavaC#中的泛型與C++的模板大體對應,但也有一些功能上的顯著差異(C++模板支持兩者沒有明確對應的模板模板參數和模板非類型參數,但不支持Java的通配符以及C#的泛型類型約束)。模板是C++的泛型編程中不可缺少的一部分。

模板是C++程序員絕佳的武器,特別是結合了多重繼承運算符重載之後。C++的標準函數庫提供的許多有用的函數大多結合了模板的概念,如STL以及iostream

語法

模板的聲明與定義

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

模板的非類型形參

模板的非類型形參(template non-type parameter)允許為下述形式:

  • 整型或枚舉型
  • 到對象的指針或函數指針
  • 到對象的引用或函數引用
  • 成員指針

模板的非類型參數被聲明為數組或函數的,將被轉換為指針或函數指針。例如:

 template<int a[4]> struct A { };
 template<int f(int)> struct B { };
 int i;
 int g(int) { return 0;}
 A<&i> x;
 B<&g> y;

模板的非類型形參允許用const或volatile限定(而模板的類型形參是不允許cv限定的)。模板的非類型形參是不允許聲明為浮點型、class類型、void型。

模板的模板參數

類模板的模板參數允許是另外一個類模板,這稱為模板的模板參數(template template parameter),也譯作「模板參數模板」。函數模板不允許有模板的模板參數。例如:

template<template <class T> class X> class A { }; //类模板A的第二个参数是另外一个类模板X
template<class T> class B { };
A<B> a; //模板A的实际使用。其中的B是模板的模板实参(template template argument)

模板參數的默認值

模板形參可以給出默認值(default arguments for template parameters)。如果一個模板參數給出了默認值,那麼模板形參列表中在其後聲明的模板參數都應該給出默認值。例如:

template<class T = char, class U, class V = int> class X { }; //编译出错,或者给出U的默认值,或者不给出T的默认值

一個模板的各次聲明給出的模板參數的默認值可以累積其效果。例如:

template<class T, class U = int> class A;
template<class T = float, class U> class A;
template<class T, class U> class A {
  public:
     T x;
     U y;
};
A<> a; //a.x is float, and the type of a.y is int

但是如果交換本示例第一行與第二行的次序,將編譯報錯。因為如果第一個模板參數T有了默認值,此時編譯器必須已經知道其後的第二個模板參數U的默認值。

在同一個作用域(scope)中,不能對同一個模板的同一個參數多次聲明其默認值。例如:

template<class T = char> class X;
template<class T = char> class X { };//编译报错。如果在本行中不给出模板参数T的默认值将编译通过

模板參數的作用域為從其聲明之處至該模板的定義結束之處。因此可以使用一個模板參數作為其後聲明的其他模板參數的一部分或默認值。例如:

template<class V, V obj> class C; 
template<class T, class U = T> class D { };

變量模板

變量模板(variable template)是C++14引入的新的一個種類的模板。可用於在命名空間作用域聲明一個變量。例如:

template<class T>
constexpr T pi = T(3.1415926535897932385);  // variable template
template<class T>
T circular_area(T r) // function template
{
   return pi<T> * r * r; // pi<T> is a variable template instantiation
}

可以在類作用域聲明一個靜態數據成員:

struct matrix_constants
{
   template<class T>
   using pauli = hermitian_matrix<T, 2>; // alias template
   template<class T> static constexpr pauli<T> sigma1 = { { 0, 1 }, { 1, 0 } }; // static data member template
   template<class T> static constexpr pauli<T> sigma2 = { { 0, -1i }, { 1i, 0 } };
   template<class T> static constexpr pauli<T> sigma3 = { { 1, 0 }, { 0, -1 } };
};

類的靜態數據成員模板,也可以用類模板的非模板數據成員來實現:

struct limits {
   template<typename T>
   static const T min; // declaration of a static data member template
};
template<typename T> const T limits::min = { }; // definition of a static data member template
template<class T> class X {
    static T s; // declaration of a non-template static data member of a class template
};
template<class T> T X<T>::s = 0; // definition of a non-template data member of a class template

變量模板不能用作模板的模板參數(template template arguments)。

模板的使用

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

如果模板參數使用其默認值,則在模板實參列表中可以忽略它。如果所有的模板參數都使用了默認值,模板實參列表為空,但仍然必須寫出成對的尖括號。例如:

template<class T = int> class X { };
X<> a; //编译通过
X b;   //编译报错

對於作為類型的模板實參,不允許是局部類型(local type)、無鏈接性的類型(type with no linkage)、無名類型(unnamed type)或包括了這三種情形的複合類型。[2]但C++11以及允許本地類型作為模板實參。

示例

函數模板

以下以取最大值的函數模板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;
   int a=3,b=7;
   float x=3.0,y=7.0;
   //Calling template function
   std::cout << maximum<int>(a,b) << std::endl;         //输出 7
   std::cout << maximum(a, b) << std::endl;             //自动补充类型声明
   std::cout << maximum<double>(x,y) << 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++標準規定:如果外圍的類模板沒有特例化,裡面的成員模板就不能特例化[3]。例如:

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

依賴名字與typename關鍵字

一個模板中的依賴於一個模板參數(template parameter)的名字被稱為依賴名字 (dependent name)。當一個依賴名字嵌套在一個類的內部時,稱為嵌套依賴名字(nested dependent name)。一個不依賴於任何模板參數的名字,稱為非依賴名字(non-dependent name)。[4]

編譯器在處理模板定義時,可能並不確定依賴名字表示一個類型,還是嵌套類的成員,還是類的靜態成員。C++標準規定:如果解析器在一個模板中遇到一個嵌套依賴名字,它假定那個名字不是一個類型,除非顯式用typename關鍵字前置修飾該名字。[5]

typename關鍵字有兩個用途:

  1. 常見的在模板定義中的模板形參列表,表示一個模板參數是類型參數。等同於使用class
  2. 使用模板類內定義的嵌套依賴類型名字時,顯式指明這個名字是一個類型名。否則,這個名字會被理解為模板類的靜態成員名。C++11起,這一用途也可以出現在模板以外,儘管此時typename關鍵字不是必要的。

在下述情形,對嵌套依賴類型名字不需要前置修飾typename關鍵字:[6]

  • 派生類聲明的基類列表中的基類標識符;
  • 成員初始化列表中的基類標識符;
  • classstructenum等關鍵字開始的類型標識符

因為它們的上下文已經指出這些標識符就是作為類型的名字。例如:

template <class T> class A: public T::Nested { //基类列表中的T::Nested
  public:
    A(int x) : T::Nested(x) {}; //成员初始化列表中的T::Nested
    struct T::type1 m; //已经有了struct关键字的T::type1
};

class B{
  public:
    class Nested{
      public:
           Nested(int x){};
    };
    typedef struct {int x;} type1;
};

int main()
{
  A<B> a(101);
  return 0;
}

template關鍵字

template關鍵字有兩個用途:

  1. 常見的在模板定義的開始。
  2. 模板類內部定義了模板成員函數或者嵌套的成員模板類。在模板中,當引用這樣的模板成員函數或嵌套的成員模板類時,可以在::(作用域解析)運算符、.(以對象方式訪問成員)運算符、->(以指針方式訪問成員)運算符之後使用template關鍵字,隨後才是模板成員函數名字或嵌套的成員模板類名字,這使得隨後的左尖括號<被解釋為模板參數列表的開始,而不是小於號運算符。C++11起,這一用途也可以出現在模板以外,儘管此時template關鍵字不是必要的。例如:
class A { public:
    template <class U> class B{
         public: typedef int INT;
    };
    template <class V> void foo(){}
};

template <typename T>
int f()
{
  typename T::template B<double>::INT i;
  i=101;
  T a, *p=&a;
  a.template foo<char>();
  p->template foo<long>();
  return 0;
}

int main()
{
  f<A>();
  A::B<double>::INT i;  // 自C++11起,也可写作typename A::template B<double>::INT i;
}

別名模板

別名模板(aliase template)是C++11引入的技術。在C++03標準中,可以用typedef給全特化模板定義新的類型名。但是不允許用typedef施加於偏特化模板上。例如:

template <typename First, typename Second, int Third>
class SomeType;

template <typename Second>
typedef SomeType<OtherType, Second, 5> TypedefName; // Illegal in C++03

C++11增加了給偏特化模板增加別名的功能,例如:

template <typename First, typename Second, int Third>
class SomeType;

template <typename Second>
using TypedefName = SomeType<OtherType, Second, 5>;

using在C++11中也可用於其他的類型別名的聲明:

typedef void (*FunctionType)(double);       // Old style
using FunctionType1 = void (*)(double); // New introduced syntax

模板實例化

模板實例化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++的模板實例化,目前分為三種方式:[7]

  • 不指定任何特殊的編譯器參數:按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關鍵字指出模板實例在其它編譯單元中顯式聲明(這已經被C++11標準接受);用inline關鍵字實例化編譯器支持的數據(如虛表)但不實例化模板成員;用static關鍵字實例化模板的靜態數據成員但不實例化其它非靜態的模板成員。
  • g++不支持模板實例化的export關鍵字(此關鍵字的這個用法已在C++11標準里被取消)。

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

參考文獻

  1. ^ 1.0 1.1 MSDN:嵌套的类模板. [2014-09-25]. (原始內容存檔於2016-05-29). 
  2. ^ §14.3.1/2 from the 2003 C++ Standard: A local type, a type with no linkage, an unnamed type or a type compounded from any of these types shall not be used as a template-argument for a template type-parameter.
  3. ^ C++11標準:§14.7.3,¶16規定:the declaration shall not explicitly specialize a class member template if its enclosing class templates are not explicitly specialized as well
  4. ^ C++11標準:§14.6,¶1
  5. ^ C++11標準§14.6,¶2規定:A name used in a template declaration or definition and that is dependent on a template-parameter is assumed not to name a type unless the applicable name lookup finds a type name or the name is qualified by the keyword typename.
  6. ^ C++11標準§14.6,¶5規定
  7. ^ Template Instantiation. [2014-09-27]. (原始內容存檔於2021-02-26).