模板实参推断和引用折叠
template <typename T> void f(T &p);
函数参数p
是一个模板类型参数T
的引用,记住两点:编译器会应用正常的引用绑定规则; const
是底层的,不是顶层的.
从左值引用函数参数推断类型
当一个函数参数是模板类型参数的一个普通(左值)引用时(即, T&),绑定规则告诉我们,只能传递给它一个左值(如,一个变量或一个返回引用类型的表达式).实参可以是const
,也可以不是.如果实参是const
的,则T
将被推断为const
类型:
template <template T> void f1(T&); // 实参必须是一个左值
// 对f1的调用使用实参所引用的类型作为模板参数类型
f1(i); // i是一个int:T是int
f1(ci); // ci是 const int: T是cosnt int
f1(5); // 错误:不能给右值
如果一个函数参数的类型是const T&
,正常的绑定规则告诉我们可以传递给它任何类型的实参:一个对象(const
或非const
), 一个临时对象或是一个字面常量值.当函数参数本身是cosnt
时,T
的类型推断结果不会是一个const
类型,const
是函数参数类型的一部分,而不是T
本身:
template <typename T> void f2(const T&); // 可接受右值
// f2中参数是 const &; 与实参中的const无关
// 在每个调用中,f2的函数参数被推断为 const int&
f2(i); // T -> int
f2(ci); // T -> int
f2(5); // 一个 const & 参数可以绑定右值; T -> int
从右值引用函数参数推断类型
当一个函数参数是一个右值引用 (形如T &&
) 时,正常绑定规则告诉我们可以传递给它一个右值.与左值引用函数参数的推断类似,推断出的T
类型是该右值实参的类型:
template <typename T> void f3(T&&);
f3(42); // T -> int
引用折叠和右值引用参数
i
是一个int
对象,我们可能认为f3(i)
这样的调用是不合法的.毕竟, i
是一个左值,而通常我们不能将一个右值引用绑定到一个左值上.但是,有两个例外:
- 第一个例外影响右值引用参数的推断如何进行.当我们将一个左值(如
i
)传递给函数的右值引用参数,且此右值引用指向模板类型参数(如T &&
)时,编译器推断模板类型参数为实参引用类型.因此,当我们调用f3(i)
时,T
被推断为int&
,而非int
.T
被推断为int&
意味着f3
的参数类型是一个int&
的`右值引用,通常我们不能直接定义一个引用的引用,但是通过类型别名或通过模板类型参数间接定义是可以的. - 第二个例外:如果我们间接创建一个引用的引用,则这些引用形成了"折叠".对于一个给定类型
x
:X& &, X& &&
和X&& &
都折叠成类型X&
- 类型
X&& &&
折叠成X&&
引用折叠只能应用于间接创建的引用的引用,如类型别名或模板参数
如果将引用折叠规则和右值引用的特殊类型推断规则组合在一起,则意味着我们可以对一个左值调用f3
.当我们将一个左值传递给f3
的(右值引用)函数参数时,编译器推断T
为一个左值引用类型:
f3(i); // 实参是一个左值, T -> int&
f3(ci); // 实参是一个左值, T -> const int&
当一个模板参数T
被推断为引用类型时,折叠规则告诉我们函数参数T&&
折叠为一个右值引用类型.
// 无效代码,用于演示
void f3<int&>(int& &&); // T是int&, 函数参数为int& &&
f3
的函数参数是T&&
且T
是int&
,因此T&&
是int& &&
,会折叠成int&
.因此即使f3
的函数参数形式是右值引用,此调用也会用一个左值引用类型来实例化f3
:
void f3<int&>(int&);
这两个规则导致:
- 如果一个函数参数是指向模板类型的右值引用(
T&&
),则它可以被绑定到一个左值上, 且 - 如果实参是一个左值,则推断出的模板实参类型将是一个左值引用,且函数参数将被实例化为一个左值引用参数
T&
这两个规则意味着:我们可以将任意类型的实参传递给T&&
类型的函数参数.
编写接受右值引用参数的模板函数
模板参数推断为一个引用类型,会有很多神奇的影响:
template <typename T> void f3(T&& val) {
T t = val; // 拷贝还是引用 ?
t = fcn(t); // 赋值只改变t还是既改变了val ?
if (val == t) {.........} // 若T为引用, 则一直为true
}
当我们对一个右值调用f3
时,例如字面常量42
,T
为int
.在此情况下,局部变量t
的类型为int
,且通过拷贝参数val
的值被初始化.当我们对t
赋值时,参数val
保持不变.
当我们对一个左值i
调用f3
时,则T
为int&
.当我们定义并初始化局部变量t
时,赋予它类型int&
.因此,对t
的初始化将其绑定到val
.当我们对t
赋值时,也同时改变了val
的值,在f3
的这个实例版本中,if
判断永远为真.