Wang's blog

右值引用

Published on

左值与右值

C++03标准

C++03标准中将表达式分为左值与右值,并且非左即右:

  • 左值:表达式结束后依然存在的持久对象,可以出现在赋值号的左侧与右侧,可以寻址
  • 右值:表达式结束后就不再存在的临时对象,只可以出现在赋值号的右侧,不可寻址

C++11标准

C++11对左值、右值的分类重新进行了定义,在C++11标准中,表达式有以下两个属性:

  • 有身份:指代某个非临时对象
  • 可被移动:可被右值引用类型匹配

根据以上两个属性可以得到4种组合:

  • 有身份,不可移动:左值,与C++03中的左值相同
  • 无身份,可移动:纯右值,与C++03中的右值相同
  • 有身份,可移动:将亡值,即生命周期即将结束的值,为C++11中新增加的类型
  • 无身份,不可移动:无意义

将亡值与纯右值合称为右值。

右值引用

C++11之前的引用类型全部为左值引用,用&符号声明。一般来说,左值引用必须绑定左值:

// num为左值
int num = 10;
// 正确,a为左值num的引用
int &a = num;
// 错误,10为右值,不能使用左值引用绑定
int &b = 10;

但是有个例外,可以将const左值引用绑定至右值:

const int &c = 10;

C++11中添加了右值引用类型,用&&符号声明,右值引用只能绑定右值:

// num为左值
int num = 10;
// 错误,右值引用不能绑定左值
int &&a = num;
// 正确,右值引用绑定纯右值
int &&b = 10;

右值引用的主要作用是用于实现移动语义与完美转发。

移动语义

在没有右值引用之前,C++中只存在“拷贝语义”,通过使用拷贝构造函数或拷贝赋值运算符(它们的参数为左值引用)可以将一个对象进行拷贝:

class A
{
public:
    A() {}
    ~A() {}
    // 拷贝构造函数
    A(const A &rhs) {}
    // 拷贝赋值运算符
    A &operator=(const A &rhs) { return *this; }
};

int main()
{
    A a1;
    // 使用拷贝构造函数进行对象拷贝
    A a2(a1);
    A a3 = a1;
    // 使用拷贝赋值运算符进行对象拷贝
    A a4;
    a4 = a1;
}

此时,如果要将一个即将析构的对象赋值给一个新对象,仍然需要先进行拷贝再进行析构,这样运行效率较低。由于原对象即将析构,因此可以将其中的资源移动至新对象,从而减少一次资源复制。

在增加了右值引用后,可以为一个类添加参数为右值引用的构造函数与赋值运算符,即移动构造函数与移动赋值运算符,通过它们可以将一个右值对象的资源移动至新对象,从而提高运行效率,这就是“移动语义”:

class A
{
public:
    A() {}
    ~A() {}
    // 移动构造函数
    A(A &&rhs) {}
    // 移动赋值运算符
    A &operator=(A &&rhs) { return *this; }
};

int main()
{
    A a1;
    // 错误,未声明拷贝构造函数
    A a2 = a1;
    // 正确,使用std::move()将a1转换为右值引用,调用移动构造函数
    A a2 = std::move(a1);

    // 正确,A()为右值,调用移动构造函数或移动赋值运算符
    A a4{A()};
    A a5;
    a5 = A();
}

在上面的例子中,a1为左值,所以如果直接执行A a2 = a1会调用拷贝构造函数,如果拷贝构造函数未定义则会报错。如果要强制调用移动构造函数,则需要使用std::move函数将a1转换为右值引用。std::move函数的实现大致如下:

template <typename T>
typename remove_reference<T>::type &&move(T &&t)
{
    return static_cast<typename remove_reference<T>::type &&>(t);
}

可以看出,它只是将输入参数强制转换为了底层类型的右值引用。

万能引用

发生类型推导(例如模板、auto)的时候,使用T&&类型表示万能引用,万能引用类型的形参能匹配任意引用类型的左值、右值。例如:

template <class T>
void func(T &&t)
{
    return;
}

int main()
{
    A a1, a2;
    // 匹配左值引用
    func(a1);
    // 匹配右值引用
    func(std::move(a2));
}

引用折叠

在模板编程中,如果模板展开后出现双重引用,则需要进行引用折叠。折叠规则为,如果任一引用为左值引用,则结果为左值引用,否则为右值引用,即:

  • A& & -> A&
  • A&& & -> A&
  • A& && -> A&
  • A&& && -> A&&

根据此规则,在万能引用的例子中,T的类型推导为:

// T被推导为A&,T&&展开后为A&,与a1相同
func(a1);
// T被推导为A&&,T&&展开后为A&&,与std::move(a2)相同
func(std::move(a2));

完美转发

无论是左值引用还是右值引用,它们本身都是左值。因此在上述使用了万能引用的函数中,如果将该参数转发给另一个函数,则无论参数是什么类型,都会当作左值传入。此时需要使用std::forward函数进行完美转发:

template <class T>
void func(T &&t)
{
    do_something(std::forward<T>(t));
}

std::forward函数的实现大致如下:

template <typename T>
T &&forward(T &param)
{
    return static_cast<T &&>(param);
}

可以看出,通过使用引用折叠,达到了当输入是左值引用时,返回左值引用,当输入是右值引用时,返回右值引用的目的,从而实现了完美转发。