1. 程式人生 > 實用技巧 >C++ Templates (1.1 初窺函式模板 A First Look at Function Templates)

C++ Templates (1.1 初窺函式模板 A First Look at Function Templates)

目錄

1.1 初窺函式模板 A First Look at Function Templates

函式模板提供了一種針對不同型別的可以被呼叫的函式行為,換句話說,一個函式模板代表一系列的函式。函式模板和普通函式很像,除了函式的一部分元素有待確定:這些元素是引數化的(parameterized)。為了方便描述,首先看一個簡單的例子。

1.1.1 定義模板 Defining the Template

以下是一個返回兩個值中最大值的函式模板:

// basics/max1.hpp
template <typename T>
T max(T a, T b)
{
      // if b<a then yield "a" else yield "b"
      return b < a ? a : b;
}

該模板定義指定了一系列返回兩個值中最大值的函式,這兩個值作為函式引數a和b進行傳遞[1]。這些引數的型別由模板引數(template parameter)T確定。如該例子中所見,模板引數必須通過如下的語法形式進行宣告:

template <comma-separated-list-of-parameters(用逗號隔開的引數列表)>

本例中,(模板)引數列表為typename T。符號<>為一對尖括號(angle brackets),請注意該符號的使用。關鍵字typename引入了一個型別引數(type parameter)。雖然這是到目前為止最常用的一種C++程式模板引數,但其他的引數也是有可能的,將在後續章節中討論(詳見第3章)。

該例中,型別引數為T,可以使用任何識別符號(identifier)作為引數名字,但使用T是典型的方式。型別引數代表任何一種引數,並且由呼叫者呼叫該函式時確定。可以使用任何型別(基礎型別(fundamental type)

類(class)等等),只要該型別提供了模板中使用的操作(operation)。本例中,型別T必須支援<操作,因為a和b通過該操作符(operator)進行比較。可能不太明顯的是,根據max()函式的定義,型別T的值必須是能夠拷貝的(copyable),以使得該值能夠被返[2]

由於歷史原因,也可以使用關鍵字class代替typename來定義一個型別引數。關鍵字typename出現相對晚一些,在C++98標準進化的過程中出現。在那之前,關鍵字class是引入型別引數的唯一方法,並且該方法依然保持有效。因此,模板函式max()可以等效地被定義成如下形式:

template <class T>
T max(T a, T b)
{
      return b < a ? a : b;
}

該情形下,這兩種定義沒有任何區別。因此,儘管使用關鍵字class,任何型別可以作為模板引數。然而,此處使用關鍵字class會有誤導性(不是隻有class的型別才能夠用於替換T,基本型別也可以),因此更建議使用關鍵字typename。不幸的是,不像類型別宣告(class type declaration),當宣告型別引數時關鍵字struct不能用於替換typename

1.1.2 使用模板 Using the Template

以下程式展示瞭如何使用max()函式模板:

// basics/max1.cpp
#include "max1.hpp"
#include <iostream>
#include <string>

int main()
{
      int i = 42;
      std::cout << "max(7,i):      " << ::max(7,i) << '\n';

      double f1 = 3.4;
      double f2 = -6.7;
      std:cout << "max(f1,f2):      " << ::max(f1,f2) << '\n';

      std::string s1 = "mathematics";
      std::string s2 = "math";
      std::cout << "max(s1,s2)      " << ::max(s1,s2) << '\n';
}

在該程式中,函式max()被呼叫三次:一次使用兩個int,一次使用兩個double,和一次使用兩個std::string。每一次,最大值被計算,該程式由如下輸出

max(7,i): 42
max(f1,f2): 3.4
max(s1,s2): mathematics

注意到每一次max()模板被呼叫時,都帶由::,這用於確保我們的max()模板在全域性名稱空間中被找到。在標準庫中有一個std::max()模板,在一定條件下可能被呼叫或者引起不明確(ambiguity)[3]

模板不會被編譯成一個可以處理任何型別的單個實體。相反,針對模板使用的每一個型別,對應每一個型別的不同實體從模板中生成[4]。因此,max()為三個型別分別進行編譯。比如,第一個max()呼叫

int i = 42;
... max(7,i)...

使用int作為模板引數T的函式模板。因此,它具有呼叫如下程式碼的語義:

int max(int a, int b)
{
      return b < a ? a : b;
}

這個使用實體型別代替模板引數的過程稱為例項化(instantiation),並生成模板的例項(instance)[5]

僅僅使用函式模板便可觸發一個例項化的過程,並不需要額外單獨請求例項化。

相似地,其他對max()的呼叫也將例項化double和std::string版的max模板,就像它們被分別以如下方式宣告和實現:

double max(double, double);
std::string max(std::string, std::string);

void也可以作為有效的模板引數,只要生成的程式碼是有效的即可。比如:

template <typename T>
T foo(T*)
{
}

void* vp = nullptr;
foo(vp); //OK: 將推匯出 voi foo(void*);

1.1.3 二階段翻譯(二次翻譯) Two-Phase Translation

如果用某個類例項化模板,但該類不支援模板中所有操作將導致編譯期錯誤(compile-time error),比如:

std::complex<float> c1, c2; //未提供<操作
...
::max(c1,c2); //編譯期錯誤

模板的“編譯”有兩個階段:

  1. 定義時(definition time)不進行例項化,模板程式碼的正確性將被檢查(但不考慮模板引數),這包括

    • 發現語法錯誤,比如丟失分號等;

    • 發現使用不依賴於模板引數的未知名字(型別名、函式名...);

    • 檢查不依賴於模板引數的靜態斷言(static assertion);

  2. 例項化期間(instantiation time),模板程式碼再一次被檢查,以確保所有的程式碼是有效的。也就是說,所有依賴於模板引數的部分被檢查了兩遍(double-checked)。
    舉個例子:

template <typename T>
void foo(T t)
{
      undeclared();       //如果undeclared()是未知的,將產生第一階段編譯器錯誤(first-phase compile-time error)
      undeclared(t);       //如果undeclared(T)是未知的,將產生第二階段編譯錯誤(second-phase compile-time error)
      static_assert(sizeof(int) > 10, "int too small");       //如果sizeof(int)<=10,將一直失敗
      static_assert(sizeof(int) > 10, "T too small");      //如果使用sizeof(T)<=10的型別T進行例項化,將失敗
}

名字被檢查兩遍的過程被稱為二階段檢查/二次檢查(two-phase lookup),該過程將在第14.3.1節中詳細討論。

一些編譯器在第一階段不執行全面檢查(full check),因此在模板程式碼至少進行一次例項化前無法發現一般的問題[6]

編譯和連結
在實踐中處理模板過程中,二階段翻譯將導致一個重要的問題:當一個函式模板被使用並觸發例項化時,編譯器(有時候)需要看模板的定義。這破壞了普通函式(ordinary function)的常規編譯和連結之間的差別,即在編譯期只需要函式的宣告(the declaration of a function)便可編譯使用函式的程式碼。處理該問題的方法在第9章中進行討論。此時,可以使用最簡單的方法:在標頭檔案中實現每一個模板

腳註


  1. 注意到max()模板依據[StepanovNotes]有意地返回“b < a ? a : b”而不是“a < b ? b : a”來確保函式正確,儘管這兩個值相等但並不等價(equivalent but not equal)。進一步討論可參考知乎:b < a ? a : b 和 a < b ? b : a 有什麼不同? ↩︎

  2. C++17以前,型別T必須是可拷貝的,才能進行引數傳遞給函式。但是自從C++17,可以傳遞臨時量(temporaries)(右值,見附錄B)儘管沒有有效的拷貝構造和移動構造。 ↩︎

  3. 比如說,如果一種引數型別在std名稱空間中被定義(如std::string),根據C++查詢規則,全域性的和std名稱空間中的max()都可以被發現(參見附錄C)。 ↩︎

  4. “一個實體適用於所有型別”的方案是可信的,但是實踐中沒有被採用(這將導致執行時效率降低)。所有的語言規則都是基於一個原則:不同的模板引數生成不同的實體。 ↩︎

  5. 在面向物件程式設計中,術語例項和例項化也被使用,指一個類的例項(concrete object of a class)。然而由於此書關於模板,該術語指使用模板的語境,除非特殊說明。 ↩︎

  6. 比如說,一些版本的Visual C++的編譯器(如Visual Studio 2013 和 2015)允許不依賴於模板引數的未宣告的名字,甚至允許一些語法缺陷(syntax flaws),比如丟失分號。 ↩︎