左值、右值,移动语义及完美转发

这是一篇关于左值,右值,完美转发,引用折叠的文章。

什么是左值,什么是右值?

左值:具有生命周期的,具有名称的值,即为它有一个具体的内存空间。

右值:没有生命周期,也成为将亡值。不指向稳定内存地址的匿名值。

基于上述特性,我们也可以用取地址符号判断,能够取到地址的是左值,不能取到的是右值。

左值与右值的理解

从字面理解,无非是表达式等号左边的值为左值,表达式右边的值为右值。

int x = 1;
int y = 2;
int z = x + y;

以上述代码为例子,x是左值,1是右值;y是左值,2是右值。

z是左值,x+y的结果是右值。

但是简单地用表达式左右来区分左右值仍然过于武断,让我们考虑下面的例子

int a = 1;
int b = a;

在line1 a显然是左值,但是line2中a却变成了右值。故而以表达式区分是有矛盾的。所以我们还是应该理解其内在含义。

在c++中 lvalue 一般表示指向一个具体地址(具体内存)的具有名称的值(具名对象)。它有一个相对稳定的内存地址,并且有一段较长的生命周期。而右值则是不指向稳定内存地址的匿名值(不具名对象)。以上述代码为例子,因为&a和&b都能够取到地址,所以a和b都是左值。

让我们考虑以下代码

#include <iostream>

using namespace std;

int x = 1;

int get_val()
{
    return x;
}

void set_val(int val)
{
    x = val;
}

int main() {
    x++; /* rvalue */
    ++x; /* lvalue */

    int *q = &++x; /* success */
    int *p = &x++; /* fail */
    int *r = &get_val(); /* fail */
    
    int y = get_val();
    set_val(6);
    auto p = &"this is a lvalue";
}

在main函数中++x和x++虽然都是对x做自增操作,但是实际上++x是对x自增后马上返回其自身,而x++是先生成x的临时复制然后才对x递增,最后返回临时复制内容。

所以我们看上述取地址操作,x++无法取到地址,而++x可以。

int *q = &++x; /* success */ int *p = &x++; /* fail */

编译器告警

test1.cpp:20:20: error: lvalue required as unary ‘&’ operand

20 | int p = &x++; / fail */

我们再看get_val这个函数,它返回的是左值还是右值呢?它虽然返回了一个全局变量x,但是实际上返回的是x的临时复制,故而是一个右值。所以 int *r = &get_val() 也会失败。

在main函数 set_val中实参6是一个右值,但是进入函数之后形参val却是一个左值。懂得函数压栈行为的同学可能知道val是会被压入寄存器,所以此时若对其取地址取到的是寄存器地址。

前置++和后置++的实现

/* 前置++ */
Integer& operator++() {
    ++value;
    return *this;
}

/* 后置++ */
Integer operator++(int) {
    Integer tmp = *this;
    ++value;
    return tcp;
}

什么是左值引用?什么是右值引用?

概念

左值引用

左值引用:左值引用是指向左值的引用。左值引用只能用单个符号&表示。常量左值引用可以绑定左值,也可以绑定右值,非常量左值引用只能绑定左值.

这样说起来有点绕,我们来看一个例子

int &x1 = 1; // compile err
const int &x2 = 11; // compile succ

编译错误:

error: cannot bind non-const lvalue reference of type ‘int&’ to an rvalue of type ‘int’

29 | int &x1 = 1; // compile err

int &x1 是非常量左值引用,1是右值,非常量左值引用只能绑定左值。1是右值,所以此时报错

const int &x2 是常量左值引用,可以绑定左值和右值。

那么这个特性有什么作用呢?在函数形参列表中具有重大意义,比如在复制搞糟函数和复制运算符函数中,通常情况下我们实现这两个函数的形参都是一个常量左值引用。

考虑以下代码:

class X {
public:
    X() {}
    X(const X&) {}
    X& operator = (const X&) { return *this; }
};

X make_x()
{
    return X();
}

int main()
{
    X x1;
    X x2(x1);
    X x3(make_x());
    x3 = make_x();
}

以上代码可以编译通过,但是让我们测试一下,

如果去除复制构造函数中的const,则 X x3(make_x()); 和 x3 = make_x(); 会报错,因为非常量左值引用无法绑定到X make_x() 产生的右值。

但是它也存在一个很大的缺点——常量性。一旦使用了常量左值引用,就表示我们无法在函数内修改该对象的内容(强制类型转换 除外)。所以需要另外一个特性来帮助我们完成这项工作,它就是右值引用

右值引用

右值引用:右值引用是指向右值的引用。右值引用使用两个符号&&表示,即&&。右值引用的主要作用是支持移动语义完美转发。通过移动语义,我们可以避免不必要的操作,提高性能。

如下示例代码:

int main() {
    int i = 0;
    int &j = i; /* lvalue ref */
    int &&k = 10; /* rvalue ref */
}

这里的k就是一个右值引用,如果k去引用i的话会引起编译错误。

右值引用的特点是可以延长右值的生命周期,看如下示例

#include <iostream>

class X {
public:
    X() { std::cout << "X ctor" << std::endl; }
    X(const X&x) { std::cout << "X copy ctor" << std::endl; }
    ~X() { std::cout << "X dtor" << std::endl; }
    void show() { std::cout << "show X" << std::endl; }
};

X make_x()
{
    X x1;
    return x1;
}

int main()
{
    int i = 0;
    int &j = i; /* lvalue ref */
    int &&k = 10; /* rvalue ref */
    X &&x2 = make_x();
    x2.show();
}

编译:

g++ rvalue_ref.cpp -g -fno-elide-constructors

  • -fno-elide-constructors: 关闭函数返回值优化(RVO),rvo优化会减少复制构造函数的调用。

测试结果:

X ctor

X copy ctor

X dtor

show X

X dtor

如果将X &&x2 = make_x()这句代码替换为X x2 = make_x()会发生几次构造。在 没有进行任何优化的情况下应该是3次构造,首先make_x函数中x1会 默认构造一次,然后return x1会使用复制构造产生临时对象,接着 X x2 = make_x()会使用复制构造将临时对象复制到x2,最后临时对象被销毁。

而如果使用了右值引用,则最后“make_x()会使用复制构造” 这一步可以省略,只会发生两次构造。可以是减少对象复制,提升程序性能。

右值引用的优化以及移动语义

我们知道右值在很多情况下都是一个临时对象,即“将亡值”。当右值被使用过后程序会马上销毁对象并释放内存,这可能的导致性能问题(即拼房地开辟和释放内存)。

看以下示例:

(未完待续)

Question

  1. ++x返回的是左值还是右值?x++呢?字符串字面量呢?

先说结论:++x返回的是左值,x++返回的是右值。字符串字面量通常是右值,除了字符串字面量之外。

++x是对x自增后马上返回其自身,而x++是先生成x的临时复制然后才对x递增,最后返回临时复制内容。

字符串字面量通常是右值,除了字符串字面量之外:

以下代码可以被编译成功。

auto p = &”this is a lvalue”;

让我们想想这是为什么?因为编译器会将字符串字面量存储到程序的数据段中,程序加载 的时候也会为其开辟内存空间,所以我们可以使用取地址符&来获取 字符串字面量的内存地址。

  1. 什么是左值引用,什么是右值引用?

先说结论:左值引用:左值引用是指向左值的引用。左值引用只能用单个符号&表示。常量左值引用可以绑定左值,也可以绑定右值,非常量左值引用只能绑定左值.

右值引用:右值引用:右值引用是指向右值的引用。右值引用使用两个符号&&表示,即&&。右值引用的主要作用是支持移动语义完美转发。通过移动语义,我们可以避免不必要的操作,提高性能。

  1. 右值引用如何提高性能?

先说结论:右值引用通过避免不必要的拷贝来提升性能。

举两个例子:

  1. 我们可以使用右值引用来接收一个函数返回的临时对象,这时候局部变量的声明期会被延长,右值引用会直接使用局部对象的内存,避免了使用这个局部变量创建一个临时对象所带来的不必要的开销。
  2. 在使用一个临时对象构建一个对象时,我们可以通过移动构造函数,使我们在构造新对象时,将一个资源从一个临时对象(右值)”移动”到新对象,而不是创建新对象的拷贝。这样可以避免不必要的拷贝操作。
  1. 介绍一下RVO?

参考资料

  1. 现代C++语言核心特性解析

现代C++语言核心特性解析.pdf

暂无评论

发送评论 编辑评论


				
|´・ω・)ノ
ヾ(≧∇≦*)ゝ
(☆ω☆)
(╯‵□′)╯︵┴─┴
 ̄﹃ ̄
(/ω\)
∠( ᐛ 」∠)_
(๑•̀ㅁ•́ฅ)
→_→
୧(๑•̀⌄•́๑)૭
٩(ˊᗜˋ*)و
(ノ°ο°)ノ
(´இ皿இ`)
⌇●﹏●⌇
(ฅ´ω`ฅ)
(╯°A°)╯︵○○○
φ( ̄∇ ̄o)
ヾ(´・ ・`。)ノ"
( ง ᵒ̌皿ᵒ̌)ง⁼³₌₃
(ó﹏ò。)
Σ(っ °Д °;)っ
( ,,´・ω・)ノ"(´っω・`。)
╮(╯▽╰)╭
o(*////▽////*)q
>﹏<
( ๑´•ω•) "(ㆆᴗㆆ)
😂
😀
😅
😊
🙂
🙃
😌
😍
😘
😜
😝
😏
😒
🙄
😳
😡
😔
😫
😱
😭
💩
👻
🙌
🖕
👍
👫
👬
👭
🌚
🌝
🙈
💊
😶
🙏
🍦
🍉
😣
Source: github.com/k4yt3x/flowerhd
颜文字
Emoji
小恐龙
花!
上一篇
下一篇