C++ Templates (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)甚至不被允许拒绝此代码。 ↩︎

posted @ 2020-08-18 21:36  失落孤舟  阅读(306)  评论(0编辑  收藏  举报