Skip to content
Gains Summary
Main Navigation 首页 / Home
C++ 编程 / C++ Programming
系统与高性能 / Systems & Performance
Web 开发 / Web Development
人工智能 / Artificial Intelligence
工业软件 / Industrial Software
其他内容 / Other Topics
C++ 编程 / C++系统与性能 / SystemsWeb 开发 / Web人工智能 / AI工业软件 / Industrial

外观

Sidebar Navigation

← C++ 编程 / C++ Programming

现代 C++ / Modern C++

1. 现代 C++ 总览:不是特性清单,而是表达能力 / Modern C++ Overview

2. 移动、转发与值类别:std::move 为什么不移动 / Move, Forward, and Value Categories

3. 类型推导、初始化与 constexpr / Type Deduction, Initialization, and constexpr

4. 模板、实例化与 SFINAE / Templates, Instantiation, and SFINAE

5. concept 与约束:把模板要求写在接口上 / Concepts and Constraints

6. 异常、RAII 与 noexcept:失败路径也要清理 / Exceptions, RAII, and noexcept

7. lambda、捕获与生命周期:回调不是免费午餐 / Lambdas, Captures, and Lifetime

8. callable wrapper 与 std::invoke:怎样保存和调用行为 / Callable Wrappers and std::invoke

9. 词汇类型与错误建模:让接口自己说话 / Vocabulary Types and Error Modeling

10. 协程与异步抽象:先理解状态机 / Coroutines and Async Abstractions

本页目录

移动、转发与值类别:std::move 为什么不移动 / Move, Forward, and Value Categories ​

std::move 这个名字很容易误导人。

它本身不搬资源。它只是把一个表达式转换成“可以被当作将亡值使用”的形式,让后续构造或赋值有机会选择移动。

1. 复制为什么可能很贵 ​

先看一个字符串:

cpp
#include <string>

std::string a = "a very long text ...";
std::string b = a;
1
2
3
4

b = a 这里需要让 b 得到一份自己的内容。

大致可以理解为:

text
a object
  |
  v
buffer: "a very long text ..."

copy to b
  |
  v
b object
  |
  v
another buffer: "a very long text ..."
1
2
3
4
5
6
7
8
9
10
11
12

复制的语义很好:

text
a 和 b 之后互不影响。
1

但如果字符串很长、容器很大,复制可能需要分配新内存并复制大量元素。

有些时候,我们并不需要保留旧对象里的内容。

例如函数返回一个临时结果,或者把一个局部对象交给另一个对象。

这时“转移资源”比“复制资源”更合适。

2. 移动是什么 ​

移动可以先理解成:

text
把对象内部拥有的资源转交给另一个对象。
1

例子:

cpp
#include <iostream>
#include <string>
#include <utility>

int main() {
    std::string a = "hello";
    std::string b = std::move(a);

    std::cout << "b = " << b << '\n';
    std::cout << "a = " << a << '\n';
}
1
2
3
4
5
6
7
8
9
10
11

大致关系:

text
before move

a object
  |
  v
buffer: hello

after move construction

b object
  |
  v
buffer: hello

a object
  |
  v
valid but unspecified state
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18

注意最后一句。

移动后,a 仍然是一个有效对象。

你可以给它重新赋值:

cpp
a = "new value";
1

但不要依赖它一定为空。

标准只保证 moved-from 对象仍然可以析构、可以赋新值、可以调用满足其前置条件的操作。

3. std::move 本身不移动 ​

看代码:

cpp
#include <iostream>
#include <string>
#include <utility>

int main() {
    std::string s = "hello";

    std::move(s);

    std::cout << s << '\n';
}
1
2
3
4
5
6
7
8
9
10
11

这段代码通常仍然打印 hello。

原因是:

text
std::move(s)
  只产生一个表达式
  这个表达式表示:s 可以被当成可移动资源来源

没有对象接收这个表达式
  就没有发生移动构造或移动赋值
1
2
3
4
5
6

真正移动发生在这里:

cpp
std::string t = std::move(s);
1

因为 t 要被构造,构造函数重载可以选择移动构造。

流程:

text
s
  |
  v
std::move(s)
  |
  v
表达式变成可移动形态
  |
  v
std::string move constructor
  |
  v
t 接管资源
1
2
3
4
5
6
7
8
9
10
11
12
13

所以更准确的说法是:

text
std::move 不是移动动作。
std::move 是允许移动的类型转换。
1
2

4. 左值和右值先用身份理解 ​

标准里的值类别很细。

入门时先抓住“有没有稳定身份”。

cpp
std::string s = "hello";

s;                 // 左值,有名字,有稳定身份
std::string{};      // 纯右值,临时结果
std::move(s);       // 将亡值,有身份,但允许资源被复用
1
2
3
4
5

可以先这样记:

text
lvalue
  有身份,通常可以取地址,可以多次使用

prvalue
  临时计算结果,常用来初始化对象

xvalue
  有身份,但被显式标记为资源可以被转移
1
2
3
4
5
6
7
8

完整分类里还有 glvalue、rvalue 等总称。

先不用急着背。

对写代码最有帮助的是:

text
普通有名字对象是左值。
std::move(x) 把表达式变成 xvalue。
临时对象通常可以绑定到右值引用或用于移动。
1
2
3

5. 右值引用变量本身是左值 ​

这句很绕,但非常重要。

看代码:

cpp
#include <string>
#include <utility>

void take(std::string&& x) {
    std::string a = x;
    std::string b = std::move(x);
}
1
2
3
4
5
6
7

x 的类型是 std::string&&。

但表达式 x 有名字。

有名字的对象表达式是左值。

所以:

cpp
std::string a = x;
1

这里更倾向于复制。

如果你确实要把 x 的资源转出去,要写:

cpp
std::string b = std::move(x);
1

原因可以这样看:

text
函数参数 x
  |
  +-- 类型:std::string&&
  |
  +-- 表达式 x:lvalue,因为它有名字
1
2
3
4
5

类型和值类别不是同一个问题。

6. 不要随便 move 还要继续读的对象 ​

移动不是销毁。

但移动后对象内容不稳定。

危险习惯:

cpp
std::string name = "Alice";
users.push_back(std::move(name));

std::cout << name << '\n'; // 不要依赖这里的内容
1
2
3
4

更合理的做法:

cpp
std::string name = "Alice";
users.push_back(std::move(name));
name = "Bob";
1
2
3

移动后重新赋值是可以的。

但把 moved-from 对象当作仍然保存原值来读,就错了。

7. const 对象通常移动不了资源 ​

看代码:

cpp
#include <string>
#include <utility>

const std::string s = "hello";
std::string t = std::move(s);
1
2
3
4
5

std::move(s) 的结果大致是 const std::string&&。

移动构造通常需要修改源对象,把内部资源转走。

但 s 是 const,不能修改。

所以这里往往不能真正移动,只能复制。

这说明:

text
std::move 不保证移动发生。
它只影响重载选择的可能性。
真正能不能移动,还要看类型、const、可用构造函数。
1
2
3

8. std::forward 不是更高级的 move ​

模板里有一种需求:

text
调用者传左值,我就按左值传下去。
调用者传右值,我就按右值传下去。
1
2

这叫转发。

例子:

cpp
#include <utility>

void sink(const std::string&); // 接收左值或只读
void sink(std::string&&);      // 接收可移动对象

template <class T>
void wrapper(T&& value) {
    sink(std::forward<T>(value));
}
1
2
3
4
5
6
7
8
9

这里的 T&& 在模板推导语境下叫转发引用。

它不是普通右值引用。

std::forward<T>(value) 会根据 T 的推导结果恢复调用者传入时的值类别。

对比:

text
std::move(x)
  无条件把 x 转成可移动形态

std::forward<T>(x)
  在模板里有条件地保留原来的左值/右值属性
1
2
3
4
5

9. 完美转发用在哪里 ​

典型场景是包装构造:

cpp
#include <memory>
#include <utility>

template <class T, class... Args>
std::unique_ptr<T> make_owner(Args&&... args) {
    return std::unique_ptr<T>(
        new T(std::forward<Args>(args)...)
    );
}
1
2
3
4
5
6
7
8
9

这和 std::make_unique 的思想类似。

调用者传什么参数,包装函数尽量不改变它们的值类别。

text
左值参数
  保持左值,不偷偷移动

右值参数
  保持右值,可以移动
1
2
3
4
5

如果这里写成 std::move(args)...,就会把所有参数都当成可移动对象。

调用者传来的左值也可能被意外移动。

10. 本篇总结 ​

移动语义的核心链路:

text
对象拥有资源
  |
  v
复制会创建另一份资源
  |
  v
移动可以转移资源
  |
  v
std::move 只是允许移动的表达式转换
  |
  v
移动构造/移动赋值才是真正执行转移的地方
1
2
3
4
5
6
7
8
9
10
11
12
13

判断时记住:

text
有名字的对象表达式通常是左值。
右值引用变量本身也是左值。
std::move 后不要依赖源对象原值。
const 对象通常不能真正移动资源。
std::forward 用于模板转发,不是普通代码里的 move 替代品。
1
2
3
4
5

下一篇讲类型推导、初始化和 constexpr。因为现代 C++ 里,值怎么传是一半,类型到底被推导成什么是另一半。

最后更新于:

Pager
上一篇1. 现代 C++ 总览:不是特性清单,而是表达能力 / Modern C++ Overview
下一篇3. 类型推导、初始化与 constexpr / Type Deduction, Initialization, and constexpr

持续记录,持续成长

Copyright © Tidenflow