右值引用

右值引用

右值引用是 C++11 引入的与 Lambda 表达式齐名的重要特性之一。它的引入解决了 C++ 中大量的历史遗留问题,消除了诸如 std::vector、std::string 之类的额外开销,也才使得函数对象容器 std::function 成为了可能。

左值、右值的纯右值、将亡值、右值

要弄明白右值引用到底是怎么一回事,必须要对左值和右值做一个明确的理解。

左值 (lvalue, left value),顾名思义就是赋值符号左边的值。准确来说,左值是表达式(不一定是赋值表达式)后依然存在的持久对象。

右值 (rvalue, right value),右边的值,是指表达式结束后就不再存在的临时对象。

而 C++11 中为了引入强大的右值引用,将右值的概念进行了进一步的划分,分为:纯右值、将亡值。纯右值 (prvalue, pure rvalue)

  • 纯粹的右值,要么是纯粹的字面量,例如 10, true;
  • 要么是求值结果相当于字面量或匿名临时对象,例如 1+2。
  • 非引用返回的临时变量、运算表达式产生的临时变量、原始字面量、Lambda 表达式都属于纯右值。

需要注意的是,字符串字面量只有在类中才是右值,当其位于普通函数中是左值。例如:

class Foo {
    const char*&& right = "this is a rvalue"; // 此处字符串字面量为右值

public:
    void bar() {
    right = "still rvalue"; // 此处字符串字面量为右值
}
};

int main() {
const char* const &left = "this is an lvalue"; // 此处字符串字面量为左值
}

将亡值 (xvalue, expiring value),是C++11 为了引入右值引用而提出的概念(因此在传统 C++
中,纯右值和右值是同一个概念),也就是即将被销毁、却能够被移动的值。

将亡值可能稍有些难以理解,我们来看这样的代码:

std::vector<int> foo() {
    std::vector<int> temp = {1, 2, 3, 4};
    return temp;
}
std::vector<int> v = foo();

在这样的代码中,就传统的理解而言,函数 foo 的返回值 temp 在内部创建然后被赋值给 v,然而 v 获得这个对象时,会将整个 temp 拷贝一份,然后把 temp 销毁,如果这个 temp 非常大,这将造成大量额外的开销(这也就是传统 C++ 一直被诟病的问题)。在最后一行中,v 是左值、foo() 返回的值就是右值(也是纯右值)。但是,v 可以被别的变量捕获到,而 foo() 产生的那个返回值作为一个临时值,一旦被 v 复制后,将立即被销毁,无法获取、也不能修改。而将亡值就定义了这样一种行为:临时的值能够被识别、同时又能够被移动。

在 C++11 之后,编译器为我们做了一些工作,此处的左值 temp 会被进行此隐式右值转换,等价于static_cast<std::vector &&>(temp),进而此处的 v 会将 foo 局部返回的值进行移动。也就是后面我们将会提到的移动语义。

右值引用和左值引用

要拿到一个将亡值,就需要用到右值引用:T &&,其中 T 是类型。右值引用的声明让这个临时值的生命周期得以延长、只要变量还活着,那么将亡值将继续存活。

C++11 提供了 std::move 这个方法将左值参数无条件的转换为右值,有了它我们就能够方便的获得一个右值临时对象,例如:

#include <iostream>
#include <string>
39
3.3 右值引用 第 3 章语言运行期的强化
void reference(std::string& str) {
    std::cout << " 左值" << std::endl;
}
void reference(std::string&& str) {
    std::cout << " 右值" << std::endl;
}
int main()
{
    std::string lv1 = "string,"; // lv1 是一个左值
    // std::string&& r1 = lv1; // 非法, 右值引用不能引用左值
    std::string&& rv1 = std::move(lv1); // 合法, std::move 可以将左值转移为右值
    std::cout << rv1 << std::endl; // string,
    const std::string& lv2 = lv1 + lv1; // 合法, 常量左值引用能够延长临时变量的生命周期
    // lv2 += "Test"; // 非法, 常量引用无法被修改
    std::cout << lv2 << std::endl; // string,string
    std::string&& rv2 = lv1 + lv2; // 合法, 右值引用延长临时对象生命周期
    rv2 += "Test"; // 合法, 非常量引用能够修改临时变量
    std::cout << rv2 << std::endl; // string,string,string,Test
    reference(rv2); // 输出左值
    return 0;
}

rv2 虽然引用了一个右值,但由于它是一个引用,所以 rv2 依然是一个左值。注意,这里有一个很有趣的历史遗留问题,我们先看下面的代码:

#include <iostream>
int main() {
    // int &a = std::move(1); // 不合法,非常量左引用无法引用右值
    const int &b = std::move(1); // 合法, 常量左引用允许引用右值
    std::cout << a << b << std::endl;
}

第一个问题,为什么不允许非常量引用绑定到非左值?这是因为这种做法存在逻辑错误:

void increase(int & v) {
    v++;
}
void foo() {
    double s = 1;
    increase(s);
}

由于 int& 不能引用 double 类型的参数,因此必须产生一个临时值来保存 s 的值,从而当
increase() 修改这个临时值时,从而调用完成后 s 本身并没有被修改。

第二个问题,为什么常量引用允许绑定到非左值?原因很简单,因为 Fortran 需要。

posted @ 2024-06-22 19:18  DyanBlog  阅读(62)  评论(0)    收藏  举报