模板实参推断和引用

模板分为类模板,函数模板,还有成员模板。

编译器会对函数模板参数进行推导。

从左值引用函数参数推断类型

当一个函数参数是模板参数类型的一个(左值)引用时(即,形如T&),绑定规则告诉我们,只能传递一个左值(如,一个变量或一个返回引用类型的表达式)。实参可以是const类型,也可以不是。如果实参是const的,则T将被推断为const类型:

1
2
3
4
template<typename T> void f1(T&);		// 实参必须是一个左值
f1(i); // i是一个int,模板参数类型T是int
f1(ci); // ci是一个const int ;模板参数类型T是const int
f1(5); // 错误:传递给一个&参数的参数必须是一个左值

如果一个函数参数的类型是const T&,正常的绑定规则告诉我们可以传递给它任何类型的实参—一个对象(const 或非const)、一个临时对象或者一个字面常量值。当函数参数本身是const时,T的类型推断的结果不会是一个const类型。const已经是函数参数类型的一部分;因此,它不会是模板参数类型的一部分。

1
2
3
4
5
6
template<typename T>void f2(const T&);										//可以接受一个右值
// f2的参数是const &,实参中的const是无关的
// 在每个调用中,f2的函数参数都被推断为const int&
f2(i); // i是一个int, 模板参数类型T是int
f2(ci); // ci是一个const int,模板参数类型T是int
f2(5); // 一个const &可以绑定到一个右值,模板参数类型T是int

从右值引用函数参数推断类型

当一个函数参数是一个右值引用类型(即,形如T&&)时,正常绑定规则告诉我们可以传递给它一个右值。当我们这样做时,类型推断过程类似普通左值引用函数参数的推断过程。推断出的T的类型是该右值实参的类型:

1
2
template<typename T> void f3(T&&);
f3(42); // 实参是一个int类型的右值;模板参数类型T是int

引用折叠和右值引用参数

假定i是一个int对象,我们可能认为像f3(i)这样的调用是不合法的。毕竟,i是一个左值,而通常我们不能将一个右值引用绑定到一个左值上。但是C++语义在正常绑定规则之外定义了两个例外规则。这两个规则是move这种标准库设施正确工作的基础。

第一个例外规则影响右值引用参数的推断如何进行。当我们将一个左值(如i)传递给函数的右值引用类型参数,且此右值引用指向模板类型参数(如T&&)时,编译器推断模板类型参数为实参的左值引用类型。因此,当我们调用f3(i)时,编译器推断T的类型为int&,而非int。

T被推断为int&好像意味着f3的函数参数应该是一个类型int&的右值引用。通常,我们不能(直接)定义一个引用的引用。但是,通过类型别名或通过模板类型参数间接定义是可以的。

在这种情况下,我们可以使用第二个绑定规则:如果我们间接创建一个引用的引用,则这些引用形成了折叠。对于一个给定的类型X:

  • X& &、X& &&和X&& &都折叠成类型X&
  • 类型X&& &&折叠成X&&

如果将引用折叠规则和右值引用的特殊类型推断规则组合在一起,则意味着我们可以对一个左值调用f3。当我们将一个左值传递给f3的(右值引用)函数参数时,编译器推断T为一个左值引用类型。

1
2
f3(i)				// 实参是一个左值,模板参数T是int&
f3(ci); // 实参是一个左值,模板参数T是一个const int&

当一个模板参数T被推断为引用类型时,折叠规则告诉我们函数参数T&&折叠 为一个左值引用类型。例如,f3(i)的实例化结果可能像下面这样:

1
2
// 无效代码,只是用于演示目的
void f3<int&>(int & &&); // 当T是int&时,函数参数为T& &&

f3的函数参数是T&&且T是int&,因此,T&&是int& &&,会折叠成int&。因此,即使f3的函数参数形式是一个右值引用(即T&&),此调用也会用一个左值引用类型(即,int&)实例化f3:

1
void f3<int&>(int&);		// 当T是int&时,函数参数折叠为int&

这两个规则导致了两个重要结果:

  • 如果一个函数参数是一个指向模板实参类型的右值引用,且函数参数被绑定到一个左值;且
  • 如果实参是一个左值,则推断出的模板实参类型将是一个左值引用,且函数参数将被实例化为一个(普通)左值引用类型参数。

另外值得注意的是,这两个规则暗示,我们可以将任意类型的实参传递给T&&类型的函数参数。对于这种类型的参数,可以传递给它右值,亦可以传递给它左值。

forward(完美转发)

某些函数需要将其一个或多个实参连同类型不变地转发给其他函数。在此情况下,我们需要保持被转发实参的所有性质,包括实参类型是否是const的以及实参是左值还是右值。

编写一个函数,它接受一个可调用表达式和两个额外实参。该函数将调用给定的可调用对象,将两个额外参数逆序传递给它。

1
2
3
4
5
6
// 接受一个可调用对象和另外两个参数的模板
// 对翻转的参数调用给定的可调用对象
template<typename F, typename T1, typename T2>
void filp1(F f, T1 t1, T2 t2) {
f(t2, t1);
}

这个函数一般情况下工作得很好,但当我们希望用它调用一个接受引用参数的函数时就会出现问题。

1
2
3
void f(int v1, int &v2) {
cout << "v1:" << v1 << " v2:" << ++v2 << endl;
}

filp1调用f时不会改变j,这是因为j是以传值调用的形式传递给flip1的。

定义能保持类型信息的函数参数

为了通过翻转函数传递一个引用,需要重写函数,使其参数能保持给定实参的“左值性”;
通过将一个函数参数定义为一个指向模板类型参数的右值引用,使得我们可以保持const属性,因为在引用类型中的const是底层。

如果一个函数参数是指向模板类型参数的右值引用,它对应的实参的const属性和左值/右值属性将得到保持。