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,在类型推导过程中,它们使用了同一个类型折叠规则:

  1. 在类型推导过程中,如果编译器遇到的参数类型为&& &&,那么它会将其会变成&&,也就是右值引用
  2. 其它参数类型& &&,&& &,& &,则会变成&,也就是左值引用
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::forwardstd::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++面试##我的求职思考#
全部评论

相关推荐

3 8 评论
分享
牛客网
牛客企业服务