C++面试 move与forward的区别
一、为了掌握move和forward,你需要区分左值和右值
左值(lvalue)是指:具有地址的变量,比如int x = 1
,对应的有左值引用:int& y = x
右值(rvalue)是指:如果一个变量不是左值,那么就是右值,比如1
,对应的有右值引用:int&&
。
注意:如果有一个变量申明为右值引用,那么该变量可以视为左值。函数返回值(值类型),一般会被编译器优化成右值,或者直接替换调用处。
class A{}; // 这里的a和b都是左值,因为它们都有对应的地址 A func(int&& a); int&& b = 1; // func(2)返回临时变量,类型为A // 因此会被编译器优化成右值,调用A的移动构造函数 // 或直接将c替换成临时变量,避免了调用A的移动构造函数 A c{func(2)};
二、右值引用只能用于绑定右值,而左值引用只能用于绑定左值。但是有一种非常特殊的情况:const左值引用,既能绑定右值,也能绑定左值。
int a{1}; int& b{a}; // b是左值引用,能够绑定到左值a,编译成功 int&& c{a}; // c是右值引用,但是却绑定到左值a,因此【编译失败】 int&& d{1}; // d是右值引用,且绑定到右值1,因此编译成功 const int& e{1}; // 由于e是const左值引用,因此它能绑定到右值1 const int& f{d}; // 由于f是const左值引用,因此它能绑定到左值d void func1(int& p); void func1(int&& p); func1(1); // 由于1是右值,因此调用了void func1(int&& p); func1(a); // 由于a是左值,因此调用了void func1(int& p); void func2(int&& p); func2(a); // 由于a是左值,但是p期望的是右值,因此【编译失败】 void func3(int& p); void func3(const int& p); void func3(int&& p); func3(1); // 由于1是右值,编译器会优先调用void func3(int&& p); func3(a); // 由于a是非const左值,编译器会优先调用void func3(int& p); // 由于const int& p能绑定左值和右值,因此下面的调用是可行的 void func4(const int& p); func4(1); func4(a); const int ca{1}; func4(ca);
三、区分右值引用(right value reference)和万能引用(universe reference)
万能引用通常出现在类型推导的场景,比如模版和auto:
// 1. 以下例子是发生在模版中的万能引用 // a可能是右值引用或左值引用 template<typename T> T func(T&& a){ return std::forward<T>(a); } // 1是右值,因此T变成了int&&,a变成了右值引用 // 函数的定义变成了:int&& func(int&& && a) // 编译器会根据模版折叠规则将int&& && a变成int&& a // 最终函数的定义变成了:int&& func(int&& a) // auto变成了int auto b{func(1)}; // b是左值,因此T变成了int&,a变成了左值引用 // 函数的定义变成了:int& func(int& && a) // 编译器会根据模版折叠规则将int& && a变成int& a // 最终函数的定义变成了:int& func(int& a) // auto变成了int auto c{func(b)}; // 2. 以下例子是发生在auto&&中的万能引用 // d有可能是左值,也有可能是右值,取决于a template<typename T> void func1(T&& a){ auto&& d{std::forward<T>(a)}; } func1(1); // 由于1是右值,因此d是右值引用 func1(c); // 由于c是左值,因此d是左值引用 // 3. 以下例子f是一个右值引用,因为没有涉及类型推导 int&& f{1};
四、无论是auto&& d还是模版中的T&& a,在类型推导过程中,它们使用了同一个类型折叠规则:
- 在类型推导过程中,如果编译器遇到的参数类型为&& &&,那么它会将其会变成&&,也就是右值引用
- 其它参数类型& &&,&& &,& &,则会变成&,也就是左值引用
template<typename T> void func1(T&& a){ // a是万能引用 auto&& d{std::forward<T>(a)}; } // 由于1是右值,因此模版类型T变成了int&& // 函数func1变成了: /* void func1(int&& && a){ auto&& d{std::forward<int&&>(a)}; } */ // 通过上述的类型规则,func1函数进一步变成了: /* void func1(int&& a){ auto&& d{std::forward<int&&>(a)}; } */ // 函数func1里面有一个万能引用auto&& d,也使用了上述规则 // auto变成了int&&,得到了以下语句 // int&& && d{std::forward<int&&>(a)}; // int&& d{std::forward<int&&>(a)}; func1(1);
五、如何理解move和forward
move和forward的内部实现本质上都调用了static_cast,它们的使用场景不同。前者会将任何一个变量无条件地转化成右值,用于move语义;而后者则会有条件地(当且仅当该变量是右值,如果输入的变量是左值,那么forward将输入的变量转化成左值)将变量转化成右值,通常用于在模版函数中转发和保留原始变量的左值和右值属性。例子如下:
class A{}; A a; // a是左值,因为能取到a的地址 // move语义,因为调用了A的移动构造函数 A b{std::move(a)}; // 将a转化成右值,并调用A的移动构造函数来构造b // 给process传入的参数有可能是左值或右值(如下例子),但是rhs是左值 // 为了保持传入参数的左值或右值特性,需要用forward template<typename T> void process(T&& rhs){ T c{std::forward<T>(rhs)}; } // a是左值,因此T变成了A&,rhs的类型变成了A& // forward将rhs转化成左值,进而调用了A的拷贝构造函数来构造c process(a); // 由于对a进行了move操作,因此传入process的参数是右值,此时的T变成了A&&,rhs的类型变成了A&& // forward将rhs转化成右值,进而调用了A的移动构造函数来构造c process(std::move(a))
std::forward
和std::move
都用到了noexcept
关键字,这个关键字的作用是告诉使用者,forward和move是不会抛异常的。对于右值引用或万能引用(universe reference),在它最后一次使用的地方加上move或forward,如下:
class A{}; void func(A&& rhs){ subfunc1(rhs); // 不是最后一次使用rhs,所以不能对其使用move subfunc2(std::move(rhs)); // 最后一次使用rhs,所以可能使用move } template<typename T> void func(T&& rhs){ subfunc3(rhs); // 不是最后一次使用rhs,所以不能对其使用forward subfunc4(std::forward<T>(rhs)); // 最后一次使用rhs,所以可能使用forward }
如果一个函数,返回类型是值类型,且返回的对象是一个同类型的右值引用对象或万能引用对象,那么需要在return语句处使用move或forward,如下所示:
Matrix operator+(Matrix&& lhs, const Matrix& rhs){ lhs += rhs; return std::move(lhs); } template<typename T> Fraction reduceAndCopy(T&& frac){ frac.reduce(); return std::forward<T>(frac); }
如果涉及到编译器返回值优化(RVO)场景,不要对局部变量使用move和forward,因为这2个函数会阻碍编译器对返回值的优化,例子如下:
// 如果涉及到RVO,编译器会将调用处的Widget变量替换成w // RVO发生的必要条件: // 1. 函数的返回值是值类型 // 2. 返回的变量是同类型的局部变量 Widget makeWidget(){ Widget w; … return w; } // 即使是满足以上2个条件,有些编译器也不会执行RVO // 但是它会将隐式地将move作用到变量w上 // 所以对w显式地使用move,显得多此一举 Widget makeWidget(){ Widget w; … return std::move(w); }#C++面试##我的求职思考#