文章目录
模板实参推断与引用
为了理解如何从函数调用进行类型推断,考虑下面例子:
template <typename T> void f(T &p);
对于模板参数是引用,需要注意两点:编译器会应用正常的引用绑定规则;对引用的 const 是底层的,不是顶层。
从左值引用函数参数推断类型
当一个函数参数是模板参数的一个左值引用时 (即 T&),我们只能传递给它一个左值。实参可以是 const 类型,也可以不是。如果实参是 const 的,则 T 被推断为 const 类型。
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 类型。
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); // T 是 int,const T& 可以绑定一个右值
从右值引用函数参数推断类型
当一个函数参数是一个右值引用时,我们可以传给它一个右值。类型推断过程类似左值引用函数参数的推断过程。推断出的 T 的类型是该右值实参的类型:
template <typename T> void f3(T&&);
f3(42); // T 是 int
引用折叠和右值引用参数
假定 i 是 int 对象,我们可能认为 f3(i) 这样是不合法的,毕竟 i 是一个左值,通常我们不能把一个左值绑定到右值引用。但是 C++在正常绑定规则之外定义了两个例外规则,允许这种绑定。这两个例外规则是 move 正确工作的基础。
第一个例外规则影响右值引用参数的推断如何进行。如上述情况,当我们调用 f3(i) 时,编译器推断 T 为 int&,而非 int。即**,当我们将一个左值传递给函数的右值引用参数,且此右值引用指向模板类型参数时,编译器推断模板类型为实参的左值引用类型**。!!注意:此右值引用指向模板类型参数 (!!!)时才满足。
T 被推断为 int&,看起来好像 f3 的函数参数应该是一个类型 int& 的右值引用。一般情况下,我们不能定义一个引用的引用。但是通过类型别名或模板类型参数间接定义是可以的。
在这种情况下,我们有第二个例外绑定规则:如果我们间接创建一个引用的引用,则这些引用形成了“折叠”。在所有情况下,引用会折叠成一个普通的左值引用类型。在新标准中,有一个例外:右值引用的右值引用为折叠成右值引用。
有以上两个规则,意味着我们可以对一个左值调用 f3。
如果一个函数参数是指向模板参数类型的右值引用(T&&),我们可以传递给它任意类型的实参。如果将一个左值传递给这样的参数,则函数参数被实例化为一个普通的左值引用 (T&)。
编写接受右值引用参数的模板函数
模板参数可以推断为一个引用类型,这一特性对模板内的代码可能有令人惊讶的影响:
template <typename T> void f3(T&& val) {
T t = val; // 拷贝还是绑定一个引用?
t = fcn(t); // 赋值只改变 t 还是既改变 t 又改变 val
if(val == t) { /* */ } // 若 T 是引用类型,则一直为 true
}
当我们对一个右值调用 f3 的时候,例如字面常量 42,T 为 int。在此情况下,局部变量 t 的类型为 int,通过拷贝参数 val 的值被初始化。当我们对 t 赋值时,参数 val 不变。
当我们对一个左值 i 调用 f3 时,T 为 int&,因此 t 的初始化被绑定到 val,改变 t 将改变 val。if 判断永远都是 true。
同样,右值引用 T&& 会与 const T& 形成重载。
理解 std::move
我们知道,通过 move 可以获得一个绑定到左值上的右值引用。由于 move 本质上可以接受任何类型的实参,因此我们可以知道它是一个函数模板。
std::move 是如何定义的
标准库是这样定义 move 的:
template <typename T>
typename remove_reference<T>::type&& move(T &&t) {
return static_cast<typename remove_reference<T>::type&&>(t);
}
我们可以发现,函数形参是 T&&,所以我们可以传递给 move 左值或者右值:
string s1("hi!"), s2;
s2 = std::move(string("bye!")); // ok,从一个右值移动数据
s2 = std::move(s1); // ok,但是在赋值后,s1 的值是不确定的
std::move 是如何工作的
在第一个赋值中,传递给 move 的实参是 string 的构造函数的右值结果。如我们所见过的,当一个右值引用函数传递一个右值时,有实参推断出的类型为被引用类型。因此,在 std::move(string(“bye!”)) 中:
- 推断出的 T 的类型为 string
- 因此,remove_reference 用 string 进行实例化
- remove_reference<string> 的 type 成员是 string
- move 的返回类型是 string&&
- move 的函数参数 t 的类型为 string&&
这个调用实例化 move<string>,即函数:string&& move(string &&t),函数返回 static_cast<string&&>(t),实际并没有发生类型转换。因此,此调用结果返回它所接受的右值引用。
考虑第二个赋值,传递给 move 的实参是一个左值:
- 推断出 T 的类型为 string&
- remove_reference 用 string& 实例化
- remove_reference<string&> 的 type 成员是 string
- move 仍然返回 string&&
- move 的函数实参 t 实例化为 string& &&,折叠为 string&。
因此,这个调用实例化 move<string&>,即:string&& move(string &t),通过类型转换,得到 string&&。
从左值 static_cast 到一个右值引用是允许的
虽然不能隐式地将一个左值类型转换为右值引用,但我们可以用 static_cast 显式地将一个左值转换为一个右值引用。
转发
某些函数需要将其一个或多个实参连同类型不变地转发给其他函数。在此情况下,我们需要保持被转发实参的所有性质,包括实参类型是否是 const 的以及实参是左值还是右值。
作为一个例子,我们将编写一个函数,它接受一个可调用表达式和两个额外实参。我们的函数将调用给定的可调用对象,将两个额外参数逆序传递给它。下面是我们的翻转函数初步模样:
// flip1 是一个不完整的实现,顶层 const 和引用丢失了
template <typename F,typename T1,typename T2>
void flip1(F f,T1 t1,T2 t2) {
f(t2,t1);
}
这个函数一般情况下没有问题。但当我们用它调用一个接受引用参数的函数时就会出现问题:
void f(int v1,int &v2) {
cout << v1 << " " << ++ v2 << endl;
}
我们通过 flip1 调用 f,f 引用参数所做的改变不会影响实参:
f(42,i); // i 的值会改变
flip1(f,j,42); // j 的值不会改变
flip1(f, j, 42) 中 j 不会改变的原因是:j 被拷贝到 flip1 函数参数 t1,然后再调用的 f,所以实际上 f 改变的是 j 的拷贝,而没有改变 j 本身。
定义能保持类型信息的函数参数
为了通过翻转函数传递一个引用,我们需要重写函数,使其参数能保持给定实参的“左值性”。更进一步,我们也希望保持参数的 const 属性。
通过将一个函数参数定义为一个指向模板类型参数的右值引用,我们可以保持其对应实参的所有类型信息。因为 const 对应引用来说,它是底层的。模板类型参数是右值引用,我们能够通过引用折叠保持翻转实参的左值/右值属性。
template <typename F,typename T1,typename T2>
void flip2(F f,T1 &&t1,T2 &&t2) {
f(t2,t1);
}
对于 flip2,当我们这样调用时 flip2(f, j, 42),j 的值将会发生改变,因为 j 是一个左值,我们将其绑定到右值引用时,T1 会被推断为 int&,引用折叠后 t1 也就是 int&,所以 t1 会被绑定到 j。
flip2 对接受左值引用的函数工作没有问题,但不能用于接受右值引用参数的函数,例如:
void g(int &&i,int &j) {
cout << i << " " << j << endl;
}
如果我们试图通过 flip2 调用 g,(无论传递给 flip2 的是左值还是右值):
flip2(g,i,42);
上述代码会出现:不能从一个左值实例化 int&& 的错误 (!! 类型为右值引用的变量,该变量本身仍是左值)。
在调用中使用 std::forward 保存类型信息
我们可以使用一个名为 forward 的新标准库来传递 flip2 的参数,它能保持原始实参的类型。定义在头文件 utility 中。forward 必须通过显式模板实参来调用。
通常情况下,我们使用 forward 传递那些定义为模板类型参数的右值引用的函数参数。通过其返回类型上的引用折叠,forward 可以保持给定实参的左值/右值属性。
使用 forward,我们可以再次重写翻转函数:
template <typename F,typename T1,typename T2>
void flip(F f,T1 &&t1,T2&& t2) {
f(std::forward<T2>(t2),std::forward<T1>(t1));
}
如果我们调用 flip(g, i, 42),i 将以 int& 类型传递给 g,42 将以 int&& 类型传递给 g。
与 std::move 相同,对 std::forward 不使用 using 声明是一个好主意。