過載與模板

過載函式

在 C++ 中,如果兩個函式的引數不同,它們可以有相同的名稱;引數不同可以是因為引數數量不同,也可以是因為任何引數的型別不同。例如:

// overloading functions
#include <iostream>
using namespace std;

int operate (int a, int b)
{
  return (a*b);
}

double operate (double a, double b)
{
  return (a/b);
}

int main ()
{
  int x=5,y=2;
  double n=5.0,m=2.0;
  cout << operate (x,y) << '\n';
  cout << operate (n,m) << '\n';
  return 0;
}
10
2.5

在此示例中,有兩個名為 operate 的函式,但其中一個函式有兩個 int 型別的引數,而另一個函式有兩個 double 型別的引數。編譯器透過檢查函式呼叫時傳遞的實參型別,來確定在每種情況下應呼叫哪個函式。如果使用兩個 int 實參呼叫,它會呼叫具有兩個 int 形參的函式;如果使用兩個 double 實參呼叫,它會呼叫具有兩個 double 形參的函式。

在此示例中,兩個函式的行為截然不同,int 版本將其引數相乘,而 double 版本則將它們相除。通常不建議這樣做。一般而言,同名函式應具有至少相似的行為,但此示例表明,它們完全有可能行為不同。兩個過載函式(即兩個同名函式)具有完全不同的定義;從任何角度來看,它們都是不同的函式,只是恰好同名而已。

請注意,函式的過載不能僅憑返回型別區分。必須至少有一個引數的型別不同。

函式模板

過載的函式可能具有相同的定義。例如:

// overloaded functions
#include <iostream>
using namespace std;

int sum (int a, int b)
{
  return a+b;
}

double sum (double a, double b)
{
  return a+b;
}

int main ()
{
  cout << sum (10,20) << '\n';
  cout << sum (1.0,1.5) << '\n';
  return 0;
}
30
2.5

在這裡,sum 函式針對不同的引數型別進行了過載,但函式體完全相同。

sum 函式可以為多種型別過載,並且它們都具有相同的函式體是有道理的。對於這類情況,C++ 提供了定義具有泛型型別的函式的能力,這被稱為函式模板。定義函式模板的語法與常規函式相同,只是在前面加上 template 關鍵字和一系列用尖括號 <> 括起來的模板引數:

template <模板引數> 函式宣告
模板引數是一系列用逗號分隔的引數。這些引數可以透過指定 classtypename 關鍵字後跟一個識別符號來成為泛型模板型別。然後,該識別符號可以在函式宣告中像常規型別一樣使用。例如,一個泛型的 sum 函式可以定義為:

1
2
3
4
5
template <class SomeType>
SomeType sum (SomeType a, SomeType b)
{
  return a+b;
}

在模板引數列表中,使用關鍵字 class 還是 typename 來指定泛型型別沒有區別(它們在模板宣告中是 100% 的同義詞)。

在上面的程式碼中,宣告 SomeType(一個在尖括號內的模板引數中的泛型型別)後,SomeType 就可以在函式定義的任何地方使用,就像其他任何型別一樣;它可以用作引數的型別、返回型別,或用來宣告該型別的新變數。在所有情況下,它都代表一個泛型型別,該型別將在模板被例項化時確定。

例項化模板是指應用模板,使用特定的型別或值為其模板引數來建立一個函式。這透過呼叫函式模板來完成,其語法與呼叫常規函式相同,但需要用尖括號指定模板實參:

名稱 <模板實參> (函式實參)
例如,上面定義的 sum 函式模板可以這樣呼叫:

1
x = sum<int>(10,20);

函式 sum<int> 只是函式模板 sum 可能的例項化之一。在這種情況下,透過在呼叫中使用 int 作為模板實參,編譯器會自動例項化一個 sum 的版本,其中每次出現的 SomeType 都被替換為 int,就好像它是這樣定義的:

1
2
3
4
int sum (int a, int b)
{
  return a+b;
}

我們來看一個實際的例子:

// function template
#include <iostream>
using namespace std;

template <class T>
T sum (T a, T b)
{
  T result;
  result = a + b;
  return result;
}

int main () {
  int i=5, j=6, k;
  double f=2.0, g=0.5, h;
  k=sum<int>(i,j);
  h=sum<double>(f,g);
  cout << k << '\n';
  cout << h << '\n';
  return 0;
}
11
2.5

在這種情況下,我們使用了 T 作為模板引數名,而不是 SomeType。這沒有區別,而且 T 實際上是泛型型別中一個非常常見的模板引數名。

在上面的示例中,我們兩次使用了函式模板 sum。第一次使用 int 型別的實參,第二次使用 double 型別的實參。編譯器每次都例項化並呼叫了相應版本的函式。

另請注意,Tsum 函式內部也被用來宣告該(泛型)型別的區域性變數:

1
T result;

因此,result 將是一個與引數 ab 以及函式返回型別相同的變數。
在這種泛型型別 T 被用作 sum 引數的特定情況下,編譯器甚至能夠自動推匯出資料型別,而無需在尖括號內顯式指定。因此,不必像下面這樣顯式指定模板實參:

1
2
k = sum<int> (i,j);
h = sum<double> (f,g);

可以簡單地寫成:
1
2
k = sum (i,j);
h = sum (f,g);

無需用尖括號括起來的型別。當然,為此,型別必須是明確的。如果呼叫 sum 時使用了不同型別的實參,編譯器可能無法自動推匯出 T 的型別。

模板是一項強大而靈活的功能。它們可以有多個模板引數,並且函式仍然可以使用常規的非模板型別。例如:

// function templates
#include <iostream>
using namespace std;

template <class T, class U>
bool are_equal (T a, U b)
{
  return (a==b);
}

int main ()
{
  if (are_equal(10,10.0))
    cout << "x and y are equal\n";
  else
    cout << "x and y are not equal\n";
  return 0;
}
x and y are equal

請注意,此示例在呼叫 are_equal 時使用了自動模板引數推導:

1
are_equal(10,10.0)

等價於:

1
are_equal<int,double>(10,10.0)

這裡不可能有歧義,因為數值字面量總是有特定的型別:除非用字尾另行指定,否則整數字面量總是產生 int 型別的值,而浮點字面量總是產生 double 型別的值。因此,10 的型別總是 int,而 10.0 的型別總是 double

非型別模板實參

模板引數不僅可以包含由 classtypename 引入的型別,還可以包含特定型別的表示式:

// template arguments
#include <iostream>
using namespace std;

template <class T, int N>
T fixed_multiply (T val)
{
  return val * N;
}

int main() {
  std::cout << fixed_multiply<int,2>(10) << '\n';
  std::cout << fixed_multiply<int,3>(10) << '\n';
}
20
30

fixed_multiply 函式模板的第二個引數是 int 型別。它看起來就像一個常規的函式引數,並且實際上也可以像常規引數一樣使用。

但存在一個主要區別:模板引數的值是在編譯時確定的,用於生成 fixed_multiply 函式的不同例項化版本,因此該引數的值在執行時永遠不會被傳遞:在 main 函式中對 fixed_multiply 的兩次呼叫,實際上呼叫了函式的兩個不同版本:一個總是乘以 2,另一個總是乘以 3。出於同樣的原因,第二個模板實參必須是常量表達式(不能傳遞變數)。
Index
目錄