模板实参推断和引用折叠

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&&Tint&,因此T&&int& &&,会折叠成int&.因此即使f3的函数参数形式是右值引用,此调用也会用一个左值引用类型来实例化f3:

void f3<int&>(int&);

这两个规则导致:

  1. 如果一个函数参数是指向模板类型的右值引用(T&&),则它可以被绑定到一个左值上, 且
  2. 如果实参是一个左值,则推断出的模板实参类型将是一个左值引用,且函数参数将被实例化为一个左值引用参数T&

这两个规则意味着:我们可以将任意类型的实参传递给T&&类型的函数参数.

编写接受右值引用参数的模板函数

模板参数推断为一个引用类型,会有很多神奇的影响:

template <typename T> void f3(T&& val) {
    T t = val;  // 拷贝还是引用 ?
    t = fcn(t); // 赋值只改变t还是既改变了val ?
    if (val == t) {.........} // 若T为引用, 则一直为true
}

当我们对一个右值调用f3时,例如字面常量42,Tint.在此情况下,局部变量t的类型为int,且通过拷贝参数val的值被初始化.当我们对t赋值时,参数val保持不变.

当我们对一个左值i调用f3时,则Tint&.当我们定义并初始化局部变量t时,赋予它类型int&.因此,对t的初始化将其绑定到val.当我们对t赋值时,也同时改变了val的值,在f3的这个实例版本中,if判断永远为真.

posted @ 2021-04-07 23:51  phr2000  阅读(126)  评论(0编辑  收藏  举报