一、 什么是左值右值
在 C++ 中,值可以根据不同的标准进行分类。按照 表达式求值结果的类别,通常分为以下 三种值类别(Value Categories):
1. 左值(Lvalue, Locator Value)
- 特点:表示 可以被取地址(可绑定到左值引用
T&
)。 - 常见情况:
- 变量名:
int x = 10;
,其中x
是左值。 - 返回左值引用的函数:
int& foo();
,foo()
是左值。 - 解引用指针:
*ptr
是左值。
- 变量名:
2. 亡值(Xvalue, eXpiring Value)
- 特点:即将被销毁的对象,可绑定到
T&&
右值引用,但仍具有 可取地址性(像左值)。 - 常见情况:
- 返回右值引用的函数:
std::move(x)
产生亡值。 - 临时对象的成员:
std::move(x).member
。
- 返回右值引用的函数:
3. 纯右值(Prvalue, Pure Rvalue)
- 特点:不能取地址,表示一个临时值,通常用于初始化和传递。
- 常见情况:
- 字面量:
42
,"hello"
,3.14
。 - 临时对象:
std::string("test")
。 - 返回非引用的函数:
std::string foo();
。
- 字面量:
但是在 C++17 之后,纯右值(Prvalue)不再有实体对象,而是用于初始化时直接构造目标对象。
可见左右值的概念很清晰,有地址的变量就是左值,没有地址的字面值、临时值就是右值。
二、 左值引用和右值引用
1. 左值引用
int a = 10;
int &ref1 = a;
int &ref2 = 5; // 编译错误
const int &ref3 = 5; // 编译通过
- 编译错误是因为,左值引用不能指向右值,但是加上const之后能解决这个问题
const T&
既能指向左值,也能指向右值,举个例子,std::vector<T>::push_back
void push_back(const T& value); // 不加const,push_back(10)就会编译错误
2. 右值引用
- 右值引用能指向右值,本质上也是把右值提升为一个左值,并定义一个右值引用通过
std::move
指向该左值: std::move
:让左值强行转换为右值,等同于static_cast<T&&>(lvalue)
int &&ref = 5;
ref++;
等同于
int a = 5;
int &&ref = std::move(a);
ref++;
继续以std::vector<T>为例
,观察到源码:
#if __cplusplus >= 201103L
void push_back(value_type&& __x) { emplace_back(std::move(__x)); }
// 说明右值引用本身是左值
push_back
接受一个对象(左值或右值),然后 拷贝(或移动) 该对象到容器中。- 如果传递的不是
T
类型的对象,需要先构造T
类型的对象,再拷贝(或移动)到容器中。
- 这里举例说明一下
emplace_back
的优势
#include <vector>
#include <iostream>
struct Foo {
Foo(int x, int y) { std::cout << "Foo(int, int)\n"; }
};
int main() {
std::vector<Foo> vec;
Foo f(1, 2); // 调用 Foo(int, int)
vec.push_back(f); // 发生一次拷贝
vec.push_back(Foo(3, 4)); // 发生一次构造 + 一次移动
v2.emplace_back(5, 6); // 直接构造,无拷贝/移动,避免了临时对象的创建
}
emplace_back
可以省略一次构建和一次析构,从而达到优化的目的- 但是我们在需要明确拷贝构造的语义的时候,需要使用
push_back
三、 实现移动语义
std::string s = "abcabc";
std::vector<std::string> vec2;
vec2.push_back(std::move(s)); // 调用移动语义的push_back方法,避免拷贝,s会失去原有值,变成空字符串
std::cout << s << '\n'; // 输出空,因为所有权被转移
因此,可移动对象在<需要拷贝且被拷贝者之后不再被需要>的场景,建议使用std::move
触发移动语义,提升性能。
绝大多数 STL 容器和工具类都正确实现了移动语义
- 所有标准容器(如
std::vector
,std::string
,std::list
,std::map
等)均正确实现了移动语义。 - 智能指针(
std::shared_ptr
)通过所有权转移实现移动语义。 - 工具类(如
std::thread
,std::fstream
)也支持移动语义,避免资源重复释放。
- 还有些STL类是
move-only
的,比如unique_ptr
,这种类只有移动构造函数,因此只能移动(转移内部对象所有权,或者叫浅拷贝),不能拷贝(深拷贝) std::array
是特例,它本质是固定大小的栈上数组,移动语义会逐个元素移动(类似拷贝)。
特例
int a = 10;
std::vector<int> vec;
vec.push_back(std::move(a));
std::cout << a << '\n'; // 输出10,说明所有权并没有被转移
- 对于内置类型(如
int
,double
,void
,nullptr
),移动操作与拷贝操作完全等价,内置类型的值直接存储在变量内存中,没有动态分配的资源(如堆内存)。 std::move(a)
实际上执行的是拷贝操作,原始变量a
的值保持不变。- 内置类型没有真正的"移动"语义,移动即拷贝
自定义类型如何实现类似 STL 的移动语义?
#include <iostream>
#include <vector>
class Array {
public:
int *ptr, siz;
Array(int siz_) {
siz = siz_;
ptr = new int[siz];
}
Array(Array &&v) noexcept {
ptr = v.ptr;
siz = v.siz;
v.ptr = nullptr; // 置空指针,防止析构时重复释放内存,实现移动语义
v.siz = 0;
}
~Array() {
delete[] ptr;
}
};
int main() {
Array arr(10);
Array arr2 = std::move(arr);
return 0;
}
标记为 noexcept
:确保容器(如 std::vector
)在扩容时优先使用移动而非拷贝。
四、完美转发
完美转发的关键机制
-
万能引用(Universal Reference)
模板参数声明为T&&
,通过类型推导和引用折叠规则,可接受左值或右值:- 传递左值时,
T
推导为T&
,T&&
折叠为T&
(左值引用)。 - 传递右值时,
T
推导为T
,T&&
保持为T&&
(右值引用)。
- 传递左值时,
-
std::forward
根据模板参数的类型,有条件地将参数转换为左值或右值引用:- 若原始参数为左值,
std::forward
返回左值引用。 - 若原始参数为右值,
std::forward
返回右值引用。
- 若原始参数为左值,
举个例子
void target(int&) { std::cout << "左值引用" << std::endl; }
void target(int&&) { std::cout << "右值引用" << std::endl; }
// 中间层函数(无完美转发)
template <typename Arg>
void bad_forward(Arg &&arg) {
target(arg); // arg 始终是左值,因为无论是左值引用还是右值引用都是左值
}
// 中间层函数(使用完美转发)
template <typename Arg>
void good_forward(Arg&& arg) {
target(std::forward<Arg>(arg));
}
int main() {
int x = 5;
bad_forward(x); // 输出 "左值引用"
bad_forward(10); // 输出 "左值引用"(错误!期望触发右值引用)
good_forward(x); // 输出 "左值引用"
good_forward(10); // 输出 "右值引用"(正确!)
}
想要完全了解完美转发的机制,仍需深入学习万能引用和引用折叠等知识点,此处不作展开
如有错误,请指正!!!
关注主包谢谢喵~~~,分享更多有用的知识