C++右值引用和完美转发

右值引用

1. 基本含义

C++中所有的表达式都是左值或者右值。右值编译器管它叫rvalue,左值编译器叫它lvalue。具体定义没太研究,但是他们有以下区别:

  1. 左值与右值的区别是左值具名,可以取址 并访问
  2. 右值不具名,通常是临时的变量,常数等。不可取址,仅在当前作用域有效,可以被移动。

一些常见的左值和右值的例子如下图。

C++右值引用和完美转发_第1张图片

2. 用处

首先,左值,左值引用晓得吧。左值引用的一个example如下。通过左值引用,我们可以:

  • 在函数内外共用同一个变量,可以实现函数内外的值同步改变.
  • 也可以减少实例的拷贝带来的时间和内存的消耗。
class A{
    int a;
public:
    A(int num){
        a = num;
    }
    A& operator=(const A& a){}
    void set(A &a){
        this->a = a.a;
    }
}

int main(){
    A a(3);
    A b(2);
    a.set(b);
    return 0;
}

在上述example中,b就是一个左值,是一个具名变量,而如果你直接调用func(A())就会报错,因为A()是一个临时变量(右值),在传参结束后就被销毁了,因此也不能引用了。此时就需要引入右值引用,右值引用在形参中用&&表示

class A{
    int a;
public:
    A(int num){
        a = num;
    }
    A& operator=(const A& a){}
    void set(A &a){
        this->a = a.a;
    }

    void set(A &&a){
        this->a = a.a;
    }

    void func(int && num){
        a = num;
    }
}

int main(){
    A a(2);
    a.set(A(3));
    a.func(3); // 3 是常数,也是右值
    return 0;
}

当然,上述的两个set函数在功能上是有重复的,我们完全可以把他们合并,这里就要提到std::move()函数了。

3. std::move()让左值变成右值

std::move()是让一个左值变成右值,比如下面的代码也是完全合法的。

class A{
    int a;
public:
    A(int num){
        a = num;
    }
    A& operator=(const A& a){}

    void set(A &&a){
        this->a = a.a;
    }

    void func(int && num){
        a = num;
    }
}

int main(){
    int c = 4;
    A a(2);
    A b(3);
    a.set(A(3));
    a.set(std::move(b)); //没有std::move会报错
    a.func(3); // 3 是常数,也是右值
    a.func(std::move(c)); //没有std::move会报错
    return 0;
}

读完这段代码再想想,仅仅依靠左值引用,上面的功能是无法实现的。

4. 用右值引用实现类型推断

当一个函数的形参是模板类型的右值引用时,那么实参的类型会进行自动推断(而不要求必须是右值),比如下面代码中的调用都是合法的。注意在正常情况下如果一个形参是右值引用,那么给函数传递一个左值是会有编译错误的,但是使用模板类型的右值引用就不会有这个问题

class A{
    int a;
public:
    A(int num){
        a = num;
    }
    A& operator=(const A& a){}

    template
    void set(T &&a){
        this->a = a.a;
    }
}
int main(){
    A a(3);
    A b(4);
    a.set(b); // 合法,b是一个左值,传递进函数后依然是左值
    a.set(A(4)); // 合法传递一个右值
}

5. 完美转发

一个表达式是左值还是右值,这个性质是会发生变化的,在上述例子中,实参A(3)虽然是右值,但是一旦传递给函数后,在set()函数内部,它就变成了左值,因为它现在不在是一个临时变量,而是成了set函数内部的局部变量。

完美转发指的是表达式被传递时能够原封不动地被转发,这里所说的原封不动,指的是变量的值、是否 const、是否为左/右值的属性均不能发生改变。完美转发需要依靠三样东西来同时完成:模板类型、std::forward()、右值引用。下面是一个example:

class A{
    int a;
public:
    A(int num){
        a = num;
    }
    A& operator=(const A& a){}

    template
    void set(T &&a){
        this->a = a.a;
        set2(std::forward(a));
    }
    
    template
    void set2(T &&a){}
}

在上述函数中,set函数的调用传参可以是左值、也可以是右值,可以含有const,可以不含const,无论是何种情况调用都是合法的,并且set函数调用set2时,会完美转发a,如果传递进set函数的是右值,那么传递给set2的依然是右值,其他属性也一样。

6. 左值和右值模板的类型推断规则

& &&
T& & &
T&& & &&

上述表格中T&代表形参是左值引用的模板类型,T&&代表形参是右值引用的模板类型,表头&和&&分别表示实参是左值还是右值,对应表中实际的类型,可以看到

  1. 当形参为左值时,无论传入的参数是什么,它实际都会变成左值。
  2. 当形参为右值时,实际的参数由实参决定

注意

右值引用过程中涉及到一个可能的隐患,即内存有效性问题。这里提供了一个例子:

C++右值引用和完美转发_第2张图片

在上述拷贝构造中,为了安全,我们必须将实参str的data设置为null,方式构造函数调用结束后str被析构,它申请的内存变为无效的情况发生。例子详见https://www.ibm.com/developerworks/cn/aix/library/1307_lisl_c11/index.html

参考文献

  1. 移动语义与完美转发 | Universal Reference https://www.sczyh30.com/posts/C-C/cpp-move-semantic/
  2. 右值引用于转移语义:https://www.ibm.com/developerworks/cn/aix/library/1307_lisl_c11/index.html
  3. 移动语义与完美转发 https://codinfox.github.io/dev/2014/06/03/move-semantic-perfect-forward/

你可能感兴趣的:(C++右值引用和完美转发)