1. 程式人生 > 實用技巧 >C++ Template (1.5 過載函式模板 Overloading Function Templates)

C++ Template (1.5 過載函式模板 Overloading Function Templates)

返回完整目錄

目錄

1.5 過載函式模板 Overloading Function Templates

和普通函式一樣,函式模板也可以被過載,也就是說,同樣的函式名可以有不同的函式定義。所以當一個名字被用作函式呼叫時,編譯器必須確定從不同的候選者中決定呼叫哪一個。這個決策過程可以相當複雜,即使在沒有模板的情況下。本小節討論當包含模板時的過載解析規則。如果讀者對沒有模板時的函式過載基本規則不熟悉,請查閱附錄C,那提供了關於過載解析規則(overload resolution rule)的詳略得當的描述。

以下簡短的程式刻畫了過載函式模板的情形:

// basics/max2.cpp

//兩個int型別值的最大值:
int max(int a, int b)
{
      return b < a ? a : b;
}

// 兩個任何型別值的最大值:
template <typename T>
T max(T a, T b)
{
      return b < a ? a : b;
}

int main()
{
      ::max(7, 42);      //呼叫兩個int值的非模板函式
      ::max(7.0, 42.0);      //呼叫max<double>,由模板引數推斷而來
      ::max('a', 'b');      //呼叫max<char>,由模板引數推斷而來
      ::max<>(7, 42);      //呼叫max<int>,由模板引數推斷而來
      ::max<double>(7, 42);      //呼叫max<double>,沒有模板引數推斷
      ::max('a', 42.7);      //呼叫兩個int值的非模板函式
}

該例子顯示,非模板函式可以與同名的函式模板共存,也可以用相同的型別進行例項化。在其他因素都相同的情況下,相比於模板生成的版本,過載解析程式(overload resolution process)更傾向於使用非模板版本。第一個呼叫便落入了這條規則:

      ::max(7, 42);      //兩個int值型別完美匹配非模板函式

如果模板生成一個更匹配的函式,就會選擇模板,這由第二個和第三個max()的呼叫佐證:

      ::max(7.0, 42.0);      //呼叫max<double>,由模板引數推斷而來
      ::max('a', 'b');      //呼叫max<char>,由模板引數推斷而來

此處,模板更匹配因為不需要從double或char轉換成int(見附錄C.2節參考更多過載解析規則)。

可以顯示指定空模板實參列表。該語法意味著只有模板能夠解析一次函式呼叫,但是所有的模板引數應該能從呼叫實參中推斷而得:

      ::max<>(7, 42);      //呼叫max<int>,由模板引數推斷而來

雖然模板型別推導不可以進行自動型別轉換,但普通函式可以進行型別轉換,因此最後一個呼叫使用非模板函式('a'和42.7均可以轉化為int):

      ::max('a', 42.7);      //只有非模板函式允許型別轉換

一個有趣的例子是過載求最大值的模板函式,並可以僅僅顯式指定返回值型別:

//basics/maxdefault4.hpp

template<typename T1, typename T2>
auto max(T1 a, T2 b)
{
      return b < a ? a : b;
}

template<typename RT, typename T1, typename T2>
RT max(T1 a, T2 b)
{
      return b < a ? a : b;
}

如果以如下方式呼叫max()

      auto a = ::max(4, 7.2);      //使用第一個模板
      auto b = ::max<long double>(7.2, 4);      //使用第二個模板

但是,當呼叫

      auto c = ::max<int>(4, 7.2);      //ERROR: 兩個模板都匹配

兩個模板均匹配,這意味著過載解析程式不會傾向於任何一個,所以導致歧義錯誤(ambiguity error)。因此,當過載函式模板時,應當確保任何呼叫時僅有一個匹配。

一個常見的情形是同時過載指標和C風格字串(C-string)的最大值模板:

//basics/max3val.cpp

#include <cstring>
#include <string>

// 兩個任意型別值的最大值
template <typename T>
T max(T a, T b)
{
      return b < a ? a : b;
}

//最大值的指標
template <typename T>
T* max(T* a, T* b)
{
      return *b < *a ? a : b;
}

//C風格字串的最大值
char const* max(char const* a, char const* b)
{
      return std::strcmp(b, a) < 0 ? a : b;
}

int main()
{
      int a = 7;
      int b = 42;
      auto m1 = ::max(a, b);      //兩個int型別值的max()

      std::string s1 = "hey";
      std::string s2 = "you";
      auto m2 = ::max(s1, s2);      //兩個std::string型別值的max()

      int* p1 = &b;
      int *p2 = &a;
      auto m3 = ::max(p1, p2);      //兩個指標的max()

      char const* x = "hello";
      char const* y = "world";
      auto m4 = ::max(x, y);      //兩個C風格字串的max()
}

這些max()的過載版本均以值進行傳遞。過載函式模板時,遵循除非有必要否則不進行改變的準則是個好主意,應當限定引數數量的改變或者顯式指定模板引數,否則會產生意想不到的效果。比如,如果實現max()模板時,以引用方式進行引數傳遞,並且過載以值傳遞兩個C風格字串,則不能使用3個引數的版本來計算三個C風格字串的最大值:

// basics/max3ref.cpp

#include <cstring>

//兩個任何型別值的最大值(以引用傳遞方式呼叫)
template <typename T>
T const& max(T const& a, T const& b)
{
      return b < a ? a : b;
}

//兩個C風格字串的最大值(以值傳遞方式呼叫)
char const* max(char const* a, char const* b)
{
      return std::strcmp(b, a) < 0 ? a : b;
}

//三個任何型別值的最大值(以引用傳遞方式呼叫)
template <typename T>
T const& max(T const& a, T const& b, T const& c)
{
      return max(max(a,b), c);      //如果max(a,b)以值傳遞方式呼叫將產生錯誤
}

int main()
{
      auto m1 = ::max(7, 42, 68);      //正確
      
      char const* s1 = "frederic";
      char const* s2 = "anica";
      char const* s3 = "lucas";
      auto m2 = ::max(s1, s2, s3);      //執行時錯誤,未定義的行為(undefined behavior)
}

該問題在於呼叫三個C風格字串的max(),該語句為:

      return max(max(a,b), c);

變為一個執行時錯誤。因為對於C風格字串,max(a,b)建立了一個新的臨時的區域性變數並以引用方式返回,但該臨時變數在返回語句完成時便終結了(expire),在主函式中留下一個懸空引用(dangling reference)。不幸的是,該錯誤相當微妙,在任何情況下可能無法清楚顯示(may not manifest itself in all cases)[1]

相反,main()函式中的第一個max()呼叫不會遇到該問題,雖然為呼叫實參建立了臨時量(7,42,68),但這些臨時量在main()函式中建立並持續到語句結束。

這僅僅是一個其行為與預期不一致的示例程式碼,這是由複雜的過載解析規則引起的。此外,確保一個函式的所有過載版本在呼叫前被宣告,這是由於當一個函式被呼叫時不是所有對應的過載函式均可見,這一點非常重要。比如,定義一個3個引數版本的max(),當針對int型別的特定的2引數版本不可見時,2引數的模板將被3引數的版本呼叫:

// basics/max4.cpp

#include <iostream>

// 兩個任意型別值的最大值
template <typename T>
T max(T a, T b)
{
      std::cout << "max<T>() \n";
      return b < a ? a : b;
}

// 三個任意型別值的最大值
template <typename T>
T max(T a, T b, T c)
{
      return max(max(a,b), c);      //使用int型別的模板版本,因為後面的宣告太遲了
}

// 兩個int值的最大值
int max(int a, int b)
{
      std::cout << "max(int, int) \n";
      return b < a ? a : b;
}

int main()
{
      ::max(47, 11, 33); //OOPS: 使用max<T>(),而不是max(int, int);
}

細節將在第13.2節中討論。

腳註


  1. 通常來說,一個合格的編譯器(a conforming compiler)甚至不被允許拒絕此程式碼。 ↩︎