这是一篇关于左值,右值,完美转发,引用折叠的文章。
什么是左值,什么是右值?
左值:具有生命周期的,具有名称的值,即为它有一个具体的内存空间。
右值:没有生命周期,也成为将亡值。不指向稳定内存地址的匿名值。
基于上述特性,我们也可以用取地址符号判断,能够取到地址的是左值,不能取到的是右值。
左值与右值的理解
从字面理解,无非是表达式等号左边的值为左值,表达式右边的值为右值。
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
- ++x返回的是左值还是右值?x++呢?字符串字面量呢?
先说结论:++x返回的是左值,x++返回的是右值。字符串字面量通常是右值,除了字符串字面量之外。
++x是对x自增后马上返回其自身,而x++是先生成x的临时复制然后才对x递增,最后返回临时复制内容。
字符串字面量通常是右值,除了字符串字面量之外:
以下代码可以被编译成功。
auto p = &”this is a lvalue”;
让我们想想这是为什么?因为编译器会将字符串字面量存储到程序的数据段中,程序加载 的时候也会为其开辟内存空间,所以我们可以使用取地址符&来获取 字符串字面量的内存地址。
- 什么是左值引用,什么是右值引用?
先说结论:左值引用:左值引用是指向左值的引用。左值引用只能用单个符号&表示。常量左值引用可以绑定左值,也可以绑定右值,非常量左值引用只能绑定左值.
右值引用:右值引用:右值引用是指向右值的引用。右值引用使用两个符号&&表示,即&&。右值引用的主要作用是支持移动语义和完美转发。通过移动语义,我们可以避免不必要的操作,提高性能。
- 右值引用如何提高性能?
先说结论:右值引用通过避免不必要的拷贝来提升性能。
举两个例子:
- 我们可以使用右值引用来接收一个函数返回的临时对象,这时候局部变量的声明期会被延长,右值引用会直接使用局部对象的内存,避免了使用这个局部变量创建一个临时对象所带来的不必要的开销。
- 在使用一个临时对象构建一个对象时,我们可以通过移动构造函数,使我们在构造新对象时,将一个资源从一个临时对象(右值)”移动”到新对象,而不是创建新对象的拷贝。这样可以避免不必要的拷贝操作。
- 介绍一下RVO?
参考资料
- 现代C++语言核心特性解析
现代C++语言核心特性解析.pdf