C++完美转发

124 阅读4分钟

完美转发

右值引用的问题

有了右值引用,看起来我们可以完美地实现移动语意了,但是,需要留意的是,我们在将右值赋给一个右值引用后,这个右值引用其实会被当成一个左值引用(毕竟移动语意本身就要求对右值引用进行修改)!类似的,右值引用的成员也是一个左值。

因此,在访问右值引用,或者在访问右值引用的成员时,必须将其转换成右值引用,否则就会被当成普通的左值引用。

// 像这样的声明赋值没有意义,实际上,a依然会成为一个左值引用
// A &&a = getTemp();
A &a = getTemp();
acceptRValueRef(std::move(a));  // OK,这里使用move把一个被当作左值引用的右值引用转成右值引用
accestRValueRef(std::forward<A>(a));    // OK,forward也能起到转为右值引用的作用

这个现象要求我们在创建移动构造函数时,必须要使用标准库中提供的std::move对右值引用的每一个成员转为右值引用,来保证移动语意。std::move会将其参数转化为一个右值引用。之所以可以进行这样的转换,是因为我们已经知道了拥有成员的对象是一个右值引用,既然成员对象的拥有者本身马上就要被销毁,那么成员对象也一定马上就会被销毁,此时将成员对象转为左值处理才是正确的。

在将右值引用传入参数为右值引用的函数时,编译器会报错,因为右值引用实际上一旦被赋给引用变量,就会被当成左值引用。要让编译器重新将其重新当成一个右值引用,必须使用std::move,std::forward将其转成右值引用。

引用折叠

为了在模板编程时,让模板能够同时处理左值和右值引用,C++11引入了引用折叠的规则:

using MyClsLRef = MyCls&;
using MyClsRRef = MyCls&&;
​
// C++11中被引用折叠规则理解为左值引用
MyClsLRef&& lRef = getMyCls();
// 下面两行是一样的,其中第一行在C++11中被引用折叠规则理解为右值引用
MyClsRRef&& rRef = getMyCls();
MyClsRRef rRef = getMyCls();
​
// 利用引用折叠规则,可以在模板编写中将参数声明为左值引用类型,这样的模板函数实际上可以同时接收
// 左值引用和右值引用
template <typename T>
void test(T&& t) { ... }
// 当T是一个右值引用时,T&&&&被折叠成右值引用
// 当T是一个左值引用时,T&&&被折叠成左值引用
// 不用考虑T不是一个引用,会有这样的考虑说明对C++不够熟悉,函数参数被声明为引用,传进来的肯定是引用

除了std::move,标准库还提供了std::forward,它的作用其实和std::move有重叠,都可以用来将变量转换为右值引用的。只不过它被规定应该专门用于“转发”场景,并且在调用时必须指定模板参数,从而可以利用引用折叠规则,将参数的左右值引用保留下来:

A getTemp() {
    return A();
}
​
// 转发函数
void forwardToTest(A&& a) {
    // do something
//    test(a);  无法通过编译,因为一旦右值引用被赋给变量,这个变量就表现成了左值引用
    test(std::forward<A&&>(a));
}
​
// 转发函数
template<typename T>
void forwardToTestTemplate(T&& a) {
//    test(a);  同样无法通过编译
    test(std::forward<T>(a));
}
​
template<typename T>
void test(T&& a) {
    
}
          
​
int main() {
    forwardToTest(getTemp());
    forwardToTestTemplate(getTemp());
    A a;
//    forwardToTest(a); 无法通过编译,因为a不是右值引用
    forwardToTestTemplate(a);   // 可以通过编译,因为模板方法有引用折叠规则
}

move和forward的区别

  • move调用时不需要提供模板参数,它仅被用于将参数强制转为右值引用;
  • forward调用时必须要提供模板参数,通常会提供这样的模板参数:forward<T&&>,这样的好处是T如果被声明为左值,转换后还是左值,T如果被声明为右值,转换后还是右值。