CPP中的左值和右值概念
Overview
- 语法上能取地址就是左值
- 有名引用本身是左值, 无名右值引用(
std::move()
的返回)是右值 const
左值引用(const T& val = 6
)引用右值, 可以编译通过。 因为与右值不指代对象, 没有”存储”的语义没有冲突- 性能上没差别, 引用都可以避免拷贝
- 左值引用可以使用
const T&
引用右值 - 右值引用可以使用
std::move()
引用左值
发展史
原始版本是说赋值号的左右边。
- CPL
- 赋值号的左边应该是个地址, 这样才能存储
- C
- lvalue
- other: rvalue和function
- CPP
- 98: lvalue(包含函数), rvalue
- 11: 开始复杂
C中的左右值
是否真正指代一个对象, 都是语法上的东西, 毕竟临时对象也是存在栈上的
能取地址就是左值
- lvalue:
locator-value
- 指代对象
- 存储地址
- 当然也可以当右值用
- locator != 可赋值
- 如
char a[2]; a = "hi";
- 如
- rvalue
- 不指代对象(虽然一定存在内存某处), 只有值是有意义的
举个例子, 一个返回对象的函数T f()
。你可以用来赋值新结构体T t = f()
, 但不能f() = xxx
, f()
就是右值。
- 左值引用, 右值引用: 指向左值/右值的引用
- 左值引用无法指向右值, 因为右值并不指代对象, 不能”存储”/修改
- 可以使用const左值引用来引用右值
- 左值引用无法指向右值, 因为右值并不指代对象, 不能”存储”/修改
- 右值引用
- 符号:
&&
- 专门引用右值, 不能引用左值, 除非:
- 使用
std::move
让右值引用可以引用左值, 虽然叫move
但至少”别名”
- 使用
- 利用右值引用可以做到修改右值的效果
- 本质上右值引用是将一个右值提升为左值, 然后利用
std::move
引用左值
- 符号:
1 | int&& ref = 4; |
有名引用本身是左值
有名引用本身是左值, 但右值引用(引用本身)可以是右值也可以是左值
1 | void f(int &&rvalue) { |
为什么说右值引用(本身)即可以是右值也可以是右值?
因为std::move()
返回的是一个右值引用, 而上面的例子说明了右值引用是个左值, 而int &&
右边又必须是个右值, 所以这种情况下std::move
返回的右值引用是个右值。
结论: 有名右值引用本身是左值, 无名右值引用本身是右值
完美转发
1 | template<typename T, typename Arg> |
- 万能引用
- 模板参数的情况下, 使用
&&
标识符以使用万能引用
- 模板参数的情况下, 使用
- 引用折叠
- 为什么
&&
可以实现万能引用? 引用的引用自动折叠, 可以看到只有T&&
才能正确转换出右值引用T& & -> T&
T&& & -> T&
T& && -> T&
T&& && -> T&&
- 为什么
本质上也是做类型转换, std::move()
转换出右值, 而std::forward<T>()
即能转换出右值也能转换出左值, 有泛型T
决定。
前面有名引用本身是左值说过有名引用都是左值。那我们拿到一个引用后(通过函数传递或临时变量等方式)要怎么把这个左值传递下去? 就可以使用std::forward<T>()
了
什么是完美转发? 比如参数通过外层的wrapper函数传递到内层的另一个函数调用, 我们希望避免参数传递过程中的拷贝构造,所以我们可以加上引用&
。但是怎么传递右值呢?, 一个方法是手动重载, 但是在参数多时就需要手动实现很多重载。这里的本质问题是, 进入第一层函数后变量就变成左值了没法move。
std::forward原理
cpp模板展开规则: 实参左值, 则T展开成
T&
。 实参为右值, 则T展开成T
1 | // 匹配实参为左值的情况 |
移动构造
比如容器扩容
当使用临时对象初始化类对象时, 如demo a = get_demo(); demo get_demo {return demo();}
, 编译器会优先调用移动构造, 如果找不到就会使用拷贝构造函数。
1 | class SuperClass{ |