C++模板与泛型编程:模板实参推断与引用,理解std::move,与转发 (std::forward)

模板实参推断与引用

​ 为了理解如何从函数调用进行类型推断,考虑下面例子:

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 声明是一个好主意。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章