6右值引用
左值和右值¶
如何判断¶
以等号左右作为区分?¶
最简单的字面理解:等号左边为左值,等号右边为右值。
如此判断,以下代码是没错的。
但情况可能会更复杂一些,这个判定规则不准确
能否取地址¶
但凡能取地址的就是一个左值。
字符串字面量是左值¶
通常字面量都是一个右值,除字符串字面量以外
- 编译器会将字符串字面量存储到程序的数据段中,程序加载的时候也会为其开辟空间,来存储这一段字符串
- 但其他的字面量都不会开辟空间,不会提前把值存下来。而是直接赋值给变量
练习¶
x++; //右值
/* 后加加
1. 先把x的值存到一个临时变量中
2. 再对x自增1
3. 然后返回临时变量。临时变量是一个将亡值,是一个右值
*/
++x; //左值
/* 前加加
1. 先对x自增1
2. 然后返回x
*/
int x = 1; //x左值; 1是右值
int get_val() {
return x;
} //返回时会发生一次复制,到一个临时变量上,因此get_val()是右值
void set_val(int val) { //val左值,生命周期是函数内
x = val;
}
int main() {
int y = get_val(); //y是左值
set_val(6); //6是右值
}
void set_val2(const int& val) {
//set_val2(6),
//val是一个右值引用,但是val却是左值
auto p = &val; //因为它可以被取地址
x = val;
}
set_val2(6); //6是右值
解释
- 函数参数都是左值
- 因为右值引用是变量类型
左值引用¶
左值引用让C++编程在一定程度上脱离了危险的指针。
非常量左值引用¶
non-const左值引用,引用的必须是一个左值,右值不行。
- non-const意味着可能会修改这块内存,因此它必须指向一块内存,而右值是没有内存的
常量左值引用¶
const左值引用,左值、右值都可以绑定
- const意味着肯定不会修改这块内存,因此可以指向一个右值
const int& x = 11; //右值也行,编译不会报错
//因此,右值11的生命周期被延长。在后续的代码中,可以简单理解为:11会替代x
int y = x;
const int x2 = 11;
//然而,这里的右值11,当语句结束后,就会被销毁
【作用】通常函数的形参都是常量左值引用,因为这样,它就又能接收左值、也能接收右值,还能避免拷贝
class X{
public:
X() {}
X(const X&) {}
X& operator=(const X&) { return *this; }
};
X make_x() { return X(); } //make_x()返回的是右值
int main() {
X x1;
X x2(x1);
X x3(make_x());
x3 = make_x();
}
【缺点】常量性,因此右值引用诞生了
- 一旦使用了常量左值引用,就表示我们无法在函数内修改该对象的内容(强制类型转换除外)
右值引用¶
右值引用只能引用右值,在类型后面加$$
作用:避免重复构造¶
对于字面量来说,右值引用可能看不出效果。但,对于临时对象,右值引用就很有用处了,可以 避免重复构造
#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应被构造了三次
X make_x() {
X x1; //x1的构造。输出:X ctor
return x1;
}
int main() {
X x2 = make_x();
/*
make_x()
1. 将x1拷贝给一个临时对象tmp。输出:X copy ctor
2. x1生命周期结束,被析构。输出:X dtor
X x2 = tmp;
1. tmp拷贝给x2。输出:X copy ctor
2. tmp析构。输出:X dtor
*/
x2.show(); //输出:show x
}
//x2被析构。输出:X dtor
因此输出:
二、开启函数返回值优化
X make_x() {
X x1; //输出:X ctor
return x1;
}
int main() {
X x2 = make_x();
//编译器的返回值优化:x1的内存直接给了x2。因此这里没有触发任何构造
x2.show(); //输出:show X
}
//输出:X dtor
因此输出:
三、禁用所有编译器优化,但使用右值引用
X make_x() {
X x1; //构造x1。输出:X ctor
return x1;
}
int main() {
X&& x2 = make_x();
/*
make_x()
1. 将x1拷贝给一个临时对象tmp。因此输出:X copy ctor
2. x1完成使命,被析构。因此输出:X dtor
X&& x2 = tmp; x2是tmp的右值引用
tmp是一个临时对象,因此是将亡值,将亡值是右值
x2对此将亡值进行引用
*/
x2.show(); //输出:show X
}
//右值引用x2生命周期结束。输出:X dtor
因此输出:
【综上】右值引用延长了右值的生命周期,从而减少了对象的复制,提升程序的性能
【如何关闭函数返回值优化?】
- GCC加命令行参数:
-fno-elide-constructors
- CMake:
set(CMAKE_CXX_FLAGS "-fno-elide-constructors")
移动语义¶
引言¶
如上面的例子上所谈,在没有编译器优化的情况下,会发生三次拷贝。如果X
是一个大对象,那是一个极大的开销、数据复制也很慢,频繁申请和释放内存还很容易产生内存碎片。
class BigMemoryPool {
public:
static const int PoolSize = 4096;
BigMemoryPool() : pool_(new char[PoolSize]) {}
~BigMemoryPool() { if (pool_ != nullptr) delete[] pool_; }
BigMemoryPool(const BigMemoryPool& other) : pool_(new char[PoolSize]) {
std::cout << "copy big memory pool." << std::endl; //一次拷贝就4kb
memcpy(pool_, other.pool_, PoolSize);
}
private:
char* pool_;
};
BigMemoryPool get_pool(const BigMemoryPool& pool) {
return pool; //copy
}
BigMemoryPool make_pool() {
BigMemoryPool pool;
return get_pool(pool);
}
int main() {
BigMemoryPool my_pool = make_pool();
}
C++的复制构造函数通常被设计为一个深拷贝,但对于某些情况,是没有必要进行深拷贝的。
比如以下情况
a
即将被销毁,因此a里的pool_
很快就不要用了- 那么
b=a
能不能不触发深拷贝,用浅拷贝即可,将a
的内存偷过来?
移动语义¶
加一个移动构造函数即可
- 在移动构造函数中,用浅拷贝即可
- 赋值时
b = std::move(a)
即可触发移动构造函数,而不是拷贝构造函数- 或者,如果
a
是一个右值,也会触发移动构造函数
class BigMemoryPool{
public:
//要偷人家东西,不能有const,因此要传non-const的右值引用类型
BigMemoryPool(BigMemoryPool&& other) noexcept {
std::cout << "move big memory pool." << std::endl;
pool_ = other.pool_; //将other的内存指针直接赋值过来
other.pool_ = nullptr; //将ohter的置空
}
}
BigMemoryPool b;
{
BigMemoryPool a;
b = std::move(a);
}
【移动】移动,就是把变量内部所管理的内存移动出来。
【默认的移动构造函数】同复制构造函数一样,编译器在一些条件下会生成一份移动构造函数
这些条件包括
- 没有任何的复制函数,包括复制构造函数和复制赋值函数
- 没有任何的移动函数,包括移动构造函数和移动赋值函数
- 没有析构函数
虽然这些条件严苛。但默认的移动构造函数和默认的复制构造函数并没有什么区别,因此不必对编译器提供的默认移动构造函数由太多期待。
【noexcept
】虽然使用移动语义在性能上有很大收益,但却有很大风险,这些风险来自异常
- 如果在移动构造函数中发生了异常,一部分资源移动成功,而有一部分资源移动失败
- 这会造成源对象和目标对象都不完整,这种情况无法预测
- 因此需要在编写移动语义函数时,建议确保函数不会抛出异常
- 如果不能确保函数会不会抛出异常,可使用
noexcept
限制该函数。如此,当函数抛异常时,程序会调用std::terminate
强行中止
值类型¶
“值类型”(左值、右值)是表达式的一种属性
- 右值不能被取地址;而左值可以
- 纯右值,即包括非字符串的字面量,如1、1.0等
graph TD
expression表达式 --> glvalue泛左值
expression表达式 --> rvalue右值
glvalue泛左值 --> lvalue左值
glvalue泛左值 --> xvalue将亡值
rvalue右值 --> xvalue将亡值
rvalue右值 --> prvalue纯右值
struct Point
{
float x;
float y;
};
//将亡值(临时对象)属于右值的范畴
auto* p = &Point(); //编译报错:error: taking address of rvalue [-fpermissive]
//使用右值引用,可以延续将亡值的生命周期,因为将亡值也有实际的一块内存
const auto& a = Point(); //引用完之后,将亡值又可以接着用,因此又是泛左值的概念
auto* p = &a; //因为可以对它取地址
书中原文(“评估”翻译成“求值”更好)
- 所谓泛左值是指一个通过评估(求值)能够确定对象、位域或函数标识的表达式。简单来说,它确定了对象或者函数的标识(具名对象)
- 纯右值是指一个通过评估(求值)能够用于初始化对象和位域,或者能够计算运算符操作数的值的表达式
- 将亡值属于泛左值的一种,它表示资源可以被重用的对象和位域,通常这是因为它们接近其生命周期的末尾,另外也可能是经过右值引用的转换产生的
- ……
如何产生将亡值¶
第一种:使用类型转换将泛左值转换为该类型的右值引用
第二种:在C++17中引入,称它为临时量实质化,指的是纯右值转换到临时对象的过程