重载与模板

重载函数

在 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
目录