引言:为什么移动语义改变了C++的编程范式
C++11引入的移动语义(Move Semantics)是现代C++最重要的语言特性之一。在移动语义出现之前,C++程序中的对象拷贝是不可避免的性能开销——无论是函数参数传递、返回值、还是容器元素的插入与重排,都会触发昂贵的深拷贝操作。对于管理动态资源(堆内存、文件句柄、网络连接等)的类而言,每一次不必要的拷贝都意味着额外的内存分配、数据复制和资源管理开销。
移动语义通过引入右值引用(rvalue reference)和移动构造/移动赋值操作,让程序能够”窃取”资源而非复制资源。这一改变不仅仅是性能优化——它从根本上改变了C++程序员思考和设计类的方式。本文将从值类别的底层概念出发,系统讲解移动语义和完美转发的完整原理,并通过大量代码示例展示在实际工程中的应用与陷阱。

值类别:理解移动语义的基石
要真正理解移动语义,首先必须厘清C++的值类别(value category)体系。许多程序员对左值和右值的理解停留在”能取地址的是左值,不能取地址的是右值”这个简化层面,但C++的值类别远比这复杂。
C++11之前的简化模型
在C++11之前,值类别只有两种:左值(lvalue)和右值(rvalue)。左值是有身份(identity)的表达式,可以取地址;右值是临时对象,不可以取地址。这个模型虽然简单,但无法精确描述移动语义需要的场景。
C++11的三分模型
C++11将值类别扩展为三种基本类别:
- 左值(lvalue):有身份、不可移动的表达式。例如变量名、解引用表达式、前置递增等。
- 亡值(xvalue / expiring value):有身份、可移动的表达式。最典型的就是
1std::move
的返回值——它仍然有身份(你可以通过原来的变量名访问它),但它的资源即将被转移。
- 纯右值(prvalue / pure rvalue):无身份、可移动的表达式。例如字面量、临时对象、返回非引用类型的函数调用。
这三种组合出两种复合类别:
- 泛左值(glvalue) = lvalue + xvalue(有身份)
- 右值(rvalue) = xvalue + prvalue(可移动)
| 类别 | 有身份 | 可移动 | 典型例子 | ||||||
|---|---|---|---|---|---|---|---|---|---|
| lvalue | ✓ | ✗ | 变量名、
、
|
||||||
| xvalue | ✓ | ✓ |
、
|
||||||
| prvalue | ✗ | ✓ |
、
、
|
理解这个分类的关键在于:右值引用可以绑定到xvalue和prvalue,也就是所有”可移动”的表达式。这就是移动语义得以实现的基础——编译器能够识别出哪些表达式的资源可以被安全地”窃取”。
右值引用与移动操作的实现
右值引用的语法与语义
右值引用使用
1 | && |
语法声明,它只能绑定到右值(xvalue或prvalue):
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18 #include <iostream>
#include <string>
void process(const std::string& lref) {
std::cout << "const lvalue ref: " << lref << std::endl;
}
void process(std::string&& rref) {
std::cout << "rvalue ref: " << rref << std::endl;
}
int main() {
std::string s = "hello";
process(s); // 调用 const lvalue ref 版本
process(std::move(s)); // 调用 rvalue ref 版本
process(std::string("temp")); // 调用 rvalue ref 版本(prvalue)
return 0;
}
这里的关键洞察是:右值引用的重载让函数能够区分”我收到了一个持久对象”和”我收到了一个临时/将要销毁的对象”。后者意味着我们可以安全地转移其资源。
移动构造函数与移动赋值运算符
以一个简单的动态数组类为例,展示拷贝语义和移动语义的完整对比:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54 #include <algorithm>
#include <utility>
class DynamicArray {
int* data_;
size_t size_;
public:
// 构造函数
DynamicArray(size_t size) : size_(size), data_(new int[size]{}) {}
// 析构函数
~DynamicArray() { delete[] data_; }
// 拷贝构造函数(深拷贝)
DynamicArray(const DynamicArray& other)
: size_(other.size_), data_(new int[other.size_]) {
std::copy(other.data_, other.data_ + other.size_, data_);
}
// 拷贝赋值运算符
DynamicArray& operator=(const DynamicArray& other) {
if (this != &other) {
delete[] data_;
size_ = other.size_;
data_ = new int[size_];
std::copy(other.data_, other.data_ + size_, data_);
}
return *this;
}
// 移动构造函数(资源窃取)
DynamicArray(DynamicArray&& other) noexcept
: data_(other.data_), size_(other.size_) {
other.data_ = nullptr; // 置空源对象,防止double free
other.size_ = 0;
}
// 移动赋值运算符
DynamicArray& operator=(DynamicArray&& other) noexcept {
if (this != &other) {
delete[] data_; // 释放自身资源
data_ = other.data_; // 窃取源对象资源
size_ = other.size_;
other.data_ = nullptr;
other.size_ = 0;
}
return *this;
}
size_t size() const { return size_; }
int& operator[](size_t i) { return data_[i]; }
const int& operator[](size_t i) const { return data_[i]; }
};
注意移动操作的几个关键细节:
- noexcept声明:移动构造和移动赋值都应标记为
1noexcept
。这是因为标准容器(如
1std::vector)在扩容时,如果移动构造函数不是noexcept的,为了保证强异常安全保证,会退回到使用拷贝构造函数。noexcept的移动操作能让容器优先使用移动而非拷贝。
- 源对象置空:移动操作后,源对象必须处于”有效但未指定”(valid but unspecified)的状态。最安全的做法是将指针置nullptr,析构时
1delete[] nullptr
是合法的无操作。
- 自移动检查:移动赋值中同样需要检查自赋值,虽然
1std::move
的典型场景下自移动罕见,但防御性编程很重要。
移动操作的性能量化
移动语义的性能收益有多大?考虑一个管理1MB内存缓冲区的对象被放入
1 | std::vector |
:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54 #include <vector>
#include <chrono>
#include <iostream>
struct BigBuffer {
static constexpr size_t BUF_SIZE = 1024 * 1024; // 1MB
char* data;
BigBuffer() : data(new char[BUF_SIZE]) {}
~BigBuffer() { delete[] data; }
// 拷贝:1MB内存复制
BigBuffer(const BigBuffer& o) : data(new char[BUF_SIZE]) {
std::copy(o.data, o.data + BUF_SIZE, data);
}
// 移动:仅3个指针/整数的赋值
BigBuffer(BigBuffer&& o) noexcept : data(o.data) {
o.data = nullptr;
}
};
int main() {
const int N = 1000;
// 拷贝语义
{
std::vector<BigBuffer> v;
v.reserve(N);
auto t1 = std::chrono::high_resolution_clock::now();
for (int i = 0; i < N; ++i)
v.push_back(BigBuffer()); // 触发拷贝
auto t2 = std::chrono::high_resolution_clock::now();
std::cout << "Copy: "
<< std::chrono::duration<double>(t2-t1).count()
<< "s\n";
}
// 移动语义
{
std::vector<BigBuffer> v;
v.reserve(N);
auto t1 = std::chrono::high_resolution_clock::now();
for (int i = 0; i < N; ++i)
v.push_back(BigBuffer()); // 触发移动
auto t2 = std::chrono::high_resolution_clock::now();
std::cout << "Move: "
<< std::chrono::duration<double>(t2-t1).count()
<< "s\n";
}
return 0;
}
// 典型输出:Copy: 0.8s, Move: 0.002s
拷贝版本需要复制1000×1MB=1GB的数据,而移动版本只进行指针赋值操作,性能差距可达数百倍。

std::move的本质:不移动,只转换
1 | std::move |
是C++中最容易被误解的组件之一。其核心误解在于:std::move并不移动任何东西。它是一个纯粹的类型转换工具——将左值强制转换为右值引用。
std::move的实现
1
2
3
4
5
6
7
8 namespace std {
template<typename T>
constexpr typename std::remove_reference<T>::type&&
move(T&& t) noexcept {
return static_cast<
typename std::remove_reference<T>::type&&>(t);
}
}
仅此而已。它做的事就是把传入的参数
1 | static_cast |
为右值引用类型。真正的”移动”发生在移动构造函数或移动赋值运算符中,而不是在
1 | std::move |
中。
常见陷阱:对移动后的对象继续使用
1
2
3
4
5
6
7
8
9
10
11 std::string s1 = "hello world";
std::string s2 = std::move(s1);
// 危险!s1此时处于"有效但未指定"的状态
// 在大多数std::string实现中,s1变为空字符串
std::cout << s1 << std::endl; // 可能输出空,也可能输出"hello world"
std::cout << s1.size() << std::endl; // 可能是0
// 安全操作:可以对移动后的对象赋新值
s1 = "new value"; // 这是安全的
std::cout << s1 << std::endl; // 输出 "new value"
移动后的对象必须被视为”有效但未指定”的状态。你可以安全地对它赋值或析构,但不能假设它还持有原来的数据。这是移动语义编程中最重要的约定。
何时使用std::move
使用
1 | std::move |
的正确时机:
- 函数返回局部对象时不需要:编译器会自动应用RVO(返回值优化)或隐式移动。
- 将对象传入”接收所有权”的函数时:
1vec.push_back(std::move(item))
表示你不再需要
1item。
- 在移动构造/移动赋值中转移成员时:
1member_(std::move(other.member_))
。
- 容器算法中传递将不再使用的元素:
1std::move(begin, end, dest)
。
错误使用
1 | std::move |
的典型场景:
1
2
3
4
5
6
7
8
9
10 std::string getName() {
std::string result = "hello";
return std::move(result); // 错误!阻止NRVO,可能更慢
}
// 正确写法:
std::string getName() {
std::string result = "hello";
return result; // 编译器应用NRVO或隐式移动
}
完美转发:引用折叠与std::forward
完美转发(Perfect Forwarding)解决的问题是:如何将参数以原始的值类别(左值或右值)传递给另一个函数?这在泛型编程中极为重要——尤其是编写包装函数和工厂函数时。
问题:转发丢失值类别
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16 void process(int& x) { std::cout << "lvalue\n"; }
void process(int&& x) { std::cout << "rvalue\n"; }
template<typename T>
void wrapper(T&& arg) {
// arg在函数体内永远是左值(它有名字!)
// 即使T推导为int&&,arg本身也是左值
process(arg); // 永远调用lvalue版本!
}
int main() {
int x = 42;
wrapper(x); // 期望输出lvalue ✓
wrapper(42); // 期望输出rvalue ✗ 实际输出lvalue
return 0;
}
这就是”转发问题”:一旦参数有了名字(绑定到函数参数),它就变成了左值,原始的值类别信息丢失了。
引用折叠规则
理解完美转发的关键在于引用折叠(reference collapsing)规则。C++11规定,当类型推导中产生引用的引用时,按以下规则折叠:
| 组合 | 折叠结果 |
|---|---|
| T& & | T& |
| T& && | T& |
| T&& & | T& |
| T&& && | T&& |
规则很简单:只要有一个左值引用参与,结果就是左值引用;只有两个右值引用叠加,结果才是右值引用。
结合模板参数推导,
1 | T&& |
被称为”转发引用”(forwarding reference,俗称万能引用):
- 传入左值时,T推导为
1int&
,
1T&&折叠为
1int& - 传入右值时,T推导为
1int
,
1T&&保持为
1int&&
std::forward的实现与原理
1
2
3
4
5
6
7
8
9
10
11
12
13 namespace std {
template<typename T>
constexpr T&& forward(typename std::remove_reference<T>::type& t) noexcept {
return static_cast<T&&>(t);
}
template<typename T>
constexpr T&& forward(typename std::remove_reference<T>::type&& t) noexcept {
static_assert(!std::is_lvalue_reference<T>::value,
"Cannot forward an rvalue as an lvalue");
return static_cast<T&&>(t);
}
}
1 | std::forward<T>(arg) |
的核心逻辑是:如果
1 | T |
是左值引用(说明原始参数是左值),则返回左值引用;如果
1 | T |
是非引用类型(说明原始参数是右值),则返回右值引用。
1 | static_cast<T&&> |
利用引用折叠规则恢复原始的值类别。
完美转发的完整示例
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25 #include <iostream>
#include <utility>
#include <memory>
class Widget {
public:
Widget() { std::cout << "default ctor\n"; }
Widget(const Widget&) { std::cout << "copy ctor\n"; }
Widget(Widget&&) noexcept { std::cout << "move ctor\n"; }
};
// 完美转发的工厂函数
template<typename T, typename... Args>
std::unique_ptr<T> make_smart(Args&&... args) {
return std::unique_ptr<T>(new T(std::forward<Args>(args)...));
}
int main() {
Widget w;
auto p1 = make_smart<Widget>(w); // copy ctor
auto p2 = make_smart<Widget>(std::move(w)); // move ctor
auto p3 = make_smart<Widget>(); // default ctor
return 0;
}
这就是
1 | std::make_unique |
和
1 | std::make_shared |
的底层原理——通过完美转发将构造函数参数原封不动地传递给被构造对象。

工程实践中的移动与转发
Rule of Five:完整资源管理类的五大操作
如果你的类管理了需要手动释放的资源,你应该定义全部五个特殊成员函数——这就是”Rule of Five”:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36 class ResourceManager {
Resource* res_;
public:
// 1. 析构函数
~ResourceManager() { release(res_); }
// 2. 拷贝构造函数
ResourceManager(const ResourceManager& other)
: res_(clone(other.res_)) {}
// 3. 拷贝赋值运算符
ResourceManager& operator=(const ResourceManager& other) {
if (this != &other) {
release(res_);
res_ = clone(other.res_);
}
return *this;
}
// 4. 移动构造函数
ResourceManager(ResourceManager&& other) noexcept
: res_(other.res_) {
other.res_ = nullptr;
}
// 5. 移动赋值运算符
ResourceManager& operator=(ResourceManager&& other) noexcept {
if (this != &other) {
release(res_);
res_ = other.res_;
other.res_ = nullptr;
}
return *this;
}
};
拷贝并交换惯用法(Copy-and-Swap Idiom)
对于同时需要拷贝语义和移动语义的类,可以使用拷贝并交换惯用法来简化实现,同时保证异常安全:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34 class StringHolder {
char* data_;
size_t size_;
friend void swap(StringHolder& a, StringHolder& b) noexcept {
using std::swap;
swap(a.data_, b.data_);
swap(a.size_, b.size_);
}
public:
StringHolder() : data_(nullptr), size_(0) {}
~StringHolder() { delete[] data_; }
// 拷贝构造
StringHolder(const StringHolder& other)
: data_(new char[other.size_]), size_(other.size_) {
std::copy(other.data_, other.data_ + size_, data_);
}
// 统一赋值运算符:按值接收参数
// 传入左值时触发拷贝构造,传入右值时触发移动构造
StringHolder& operator=(StringHolder other) noexcept {
swap(*this, other); // 交换资源
return *this; // other析构时自动释放旧资源
}
// 移动构造
StringHolder(StringHolder&& other) noexcept
: StringHolder() {
swap(*this, other);
}
};
这种模式的优势在于赋值运算符的实现天然具有强异常安全保证——因为资源的交换在参数构造阶段就已完成,交换操作本身是noexcept的。
移动语义与标准容器
移动语义对标准容器的性能影响深远。以下是最典型的几个场景:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27 #include <vector>
#include <string>
#include <algorithm>
int main() {
std::vector<std::string> vec;
// 场景1:push_back的右值重载
vec.push_back(std::string("temporary")); // 移动构造
std::string s = "persistent";
vec.push_back(s); // 拷贝构造
vec.push_back(std::move(s)); // 移动构造,s变为空
// 场景2:vector扩容时的元素搬迁
// 如果元素的移动构造是noexcept的,扩容使用移动
// 否则退回拷贝以保证强异常安全
vec.reserve(1000); // 可能触发元素搬迁
// 场景3:sort等算法中的元素交换
std::vector<std::string> v2 = {"z", "a", "m", "b"};
std::sort(v2.begin(), v2.end()); // 内部使用移动交换
// 场景4:emplace直接构造,避免临时对象
vec.emplace_back(10, 'x'); // 直接在容器内构造"xxxxxxxxxx"
return 0;
}
高级主题与常见陷阱
返回值优化(RVO)与移动语义的交互
C++17对RVO做了强制性规定:当返回一个与函数返回类型相同的临时对象(prvalue)时,必须省略拷贝/移动(guaranteed copy elision)。但C++17的RVO只适用于prvalue,不适用于命名变量:
1
2
3
4
5
6
7
8
9
10
11
12 std::string createString() {
std::string s = "hello";
// ... 修改s ...
return s; // C++11/14: 可能NRVO或隐式移动
// C++17+: NRVO优先,无法NRVO时隐式移动
// 注意:不要写 return std::move(s)!
}
std::string createString2() {
return std::string("hello"); // C++17: 强制省略(guaranteed elision)
// 连移动构造都不调用
}
万能引用与类型推导的陷阱
1 | T&& |
只有在模板参数推导的上下文中才是万能引用。以下场景中
1 | T&& |
不是万能引用:
1
2
3
4
5
6
7
8
9
10
11
12
13 // 1. 非模板上下文中的T&&是普通右值引用
auto&& x = 42; // auto推导:x是int&&,这是万能引用
int&& y = 42; // 非推导:y是普通右值引用
// 2. 类模板的T&&成员不是万能引用
template<typename T>
class Wrapper {
T&& ref_; // 这不是万能引用!T在类实例化时已确定
};
// 3. 使用auto&&做完美转发
auto&& universal = getValue();
process(std::forward<decltype(universal)>(universal));
移动失败的隐式退回拷贝
当一个类没有定义移动操作时,编译器不会自动生成默认的移动构造/赋值。此时,按值传递或
1 | std::move |
会退回到拷贝语义:
1
2
3
4
5
6
7
8
9
10
11
12
13
14 class LegacyClass {
public:
LegacyClass() = default;
LegacyClass(const LegacyClass&) = default;
LegacyClass& operator=(const LegacyClass&) = default;
// 没有定义移动操作!
// 声明了拷贝操作后,编译器不会隐式生成移动操作
};
LegacyClass a;
LegacyClass b = std::move(a); // 调用拷贝构造!不是移动构造
// 因为没有移动构造函数,std::move的右值引用
// 匹配到 const LegacyClass& 的拷贝构造
更隐蔽的情况:当类中有成员不支持移动时,即使你声明了
1 | = default |
的移动操作,它也会被隐式删除:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16 class NonMovable {
public:
NonMovable() = default;
NonMovable(const NonMovable&) = default;
NonMovable(NonMovable&&) = delete; // 显式删除移动
};
class Container {
NonMovable obj_;
public:
Container(Container&&) = default; // 实际被隐式删除!
// 因为NonMovable不可移动,Container的移动构造被删除
};
// Container c1;
// Container c2 = std::move(c1); // 编译错误!移动构造被删除
std::forward的正确与错误用法
1 | std::forward |
只应在转发引用(
1 | T&& |
模板参数)的上下文中使用。以下是一些常见错误:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26 // 错误1:对非转发引用使用std::forward
void bad(int& x) {
process(std::forward<int&&>(x)); // 可以编译但语义错误
// 应该直接用 process(std::move(x)) 如果确定要移动
}
// 错误2:多次转发同一个参数
template<typename T>
void bad_multi(T&& x) {
process1(std::forward<T>(x)); // x可能已被移动!
process2(std::forward<T>(x)); // 使用已移动的对象,UB风险
}
// 正确:只转发一次
template<typename T>
void good(T&& x) {
process(std::forward<T>(x)); // 转发后不再使用x
}
// 正确:需要多次使用时先保存
template<typename T>
void good_multi(T&& x) {
auto saved = x; // 先拷贝/移动一份
process1(std::forward<T>(x));
process2(saved); // 使用保存的副本
}

C++20/23的移动语义新特性
C++20:CTAD与移动语义
C++20的类模板参数推导(CTAD)与移动语义的交互值得注意。当使用CTAD时,移动语义的行为可能不符合直觉:
1
2
3
4
5 #include <vector>
// C++20 CTAD
std::vector v1 = {1, 2, 3}; // 推导为 vector<int>
std::vector v2 = std::move(v1); // 移动构造
C++23:std::forward_like与移动语义的完善
C++23对移动语义的改进主要体现在标准库层面。P2644R1引入了
1 | std::is_implicit_lifetime |
特性,简化了隐式生命周期类型的判断。另一个重要的C++23改进是
1 | std::forward_like |
(P2445R1),它解决了非转发引用上下文中需要按照某个值类别转发的需求:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19 #include <utility>
#include <tuple>
#include <string>
template<typename Tuple>
auto get_and_forward(Tuple&& t) {
// C++23: 按照Tuple的值类别转发get的结果
auto& element = std::get<0>(t);
// 如果t是右值,element也是右值引用
return std::forward_like<Tuple>(element);
}
// 使用场景
template<typename Pair>
auto extract_first(Pair&& p) {
// C++23之前需要复杂的SFINAE
// C++23: 直接使用forward_like
return std::forward_like<Pair>(p.first);
}
移动语义与并发编程的交互
在多线程环境中,移动语义的”窃取”语义与线程安全需要特别关注:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30 #include <memory>
#include <mutex>
template<typename T>
class ThreadSafeContainer {
mutable std::mutex mtx_;
std::unique_ptr<T> data_;
public:
// 线程安全的移动构造
ThreadSafeContainer(ThreadSafeContainer&& other) noexcept {
std::lock_guard<std::mutex> lock(other.mtx_);
data_ = std::move(other.data_);
}
// 线程安全的移动赋值
ThreadSafeContainer& operator=(ThreadSafeContainer&& other) noexcept {
if (this != &other) {
std::scoped_lock lock(mtx_, other.mtx_); // C++17
data_ = std::move(other.data_);
}
return *this;
}
// 线程安全的提取
std::unique_ptr<T> take() {
std::lock_guard<std::mutex> lock(mtx_);
return std::move(data_);
}
};
注意移动操作中需要同时锁定源对象的互斥量,以确保并发访问时不会出现数据竞争。
最佳实践总结
综合以上讨论,以下是移动语义与完美转发在现代C++工程中的最佳实践清单:
- 默认使用
1std::unique_ptr
管理独占资源
:它天然不可拷贝、可移动,是最安全的资源管理方式。 - 移动操作标记
:这是让标准容器优先使用移动而非拷贝的前提条件。1noexcept
- 不要对返回值使用
:编译器的NRVO和隐式移动规则已经足够,手动1std::move1std::move
反而可能阻碍优化。
- 只在明确放弃所有权时使用
:例如1std::move1push_back(std::move(item))
。
- 在转发引用上下文中使用
:在1std::forward1T&&
模板参数中转发值类别。
- 不要多次转发同一个参数:转发可能导致移动,二次转发是未定义行为。
- 注意移动后的对象状态:只对移动后的对象赋值或析构,不要假设其内容。
- 使用
1= default
代替手动实现
:当所有成员都可移动时,1= default的移动操作是正确的且更安全。
- 在多线程环境中保护移动操作:移动构造/赋值也需要加锁保护源对象。
- 关注C++23的
:简化非标准转发场景的实现。1std::forward_like
移动语义和完美转发是现代C++性能编程的基础。理解值类别、引用折叠和
1 | std::forward |
的底层原理,不仅能帮助你写出更高效的代码,更能避免因误解而引入的微妙bug。当你能准确回答”这个表达式的值类别是什么?它被绑定到了哪种引用?转发后值类别是否保持一致?”这三个问题时,你就真正掌握了移动语义与完美转发的精髓。
汤不热吧