第一篇我们讲了统一初始化,重点是:C++11 让对象更自然地出生。
这一篇要接着回答另一个更核心的问题:
对象创建出来以后,在函数调用、返回值、容器插入、模板封装这些流动过程中,能不能少做一些没必要的深拷贝?
如果说第一篇在解决"怎么创建对象",那么第二篇真正要解决的是:
对象怎么更高效地流动。
而 C++11 给出的答案,就是这一整套能力:
- 右值引用
std::move- 移动构造与移动赋值
- 完美转发
std::forward
这几个概念第一次学时很容易绕,因为它们不只是新语法,而是在重新定义:什么对象适合被复制,什么对象适合被转移,什么语义应该被原样保留下来。
一、为什么"拷贝"会成为问题
对于内置类型,拷贝几乎没什么讨论价值:
cpp
int a = 10;
int b = a;
但如果对象内部持有资源,问题就变了。比如:
- 堆内存
- 文件句柄
- 动态缓冲区
- 大对象容器
- 独占资源包装类
这时一次拷贝,往往意味着:
- 重新申请资源
- 把旧内容复制过去
- 维护两份独立状态
- 将来再分别释放
所以"拷贝"不再只是复制几个字节,而是一套完整的资源复制流程。
最容易出现额外拷贝的场景通常有这几类:
| 场景 | 为什么可能贵 |
|---|---|
| 函数传值 | 实参要构造成形参 |
| 函数返回值 | 返回对象要交给调用者 |
| 容器插入元素 | 容器内部要保存自己的那份对象 |
| 容器扩容搬迁元素 | 旧元素要转移到新空间 |
问题的关键不在于"拷贝对不对",而在于:
有没有一些对象,本来就快没用了,却还要被老老实实深拷贝一遍?
C++11 主要优化的,就是这类场景。
二、左值和右值,到底在区分什么
很多人一学右值引用就卡在"左值""右值"这两个词上。真正实用的理解方式其实不复杂。
1. 一个足够好用的直觉
| 概念 | 更容易理解的说法 |
|---|---|
| 左值 | 有身份、后面还可能继续使用的对象 |
| 右值 | 临时结果、通常快没用了的对象 |
看例子:
cpp
int a = 1;
int b = 2;
a; // 左值
b; // 左值
10; // 右值
a + b; // 右值
对象类型也一样:
cpp
std::string s = "hello";
s; // 左值
std::string("hello"); // 右值
2. 为什么语言要区分左右值
因为编译器需要知道一件很重要的事:
- 这个对象后面还会不会继续被用
- 我现在应不应该复制它
- 我能不能安全地接管它的资源
所以右值真正的价值不在"它是个临时量",而在于:
临时对象通常是最适合被移动的对象。
三、const T& 都能绑定右值了,为什么还需要 T&&
这是这一篇必须讲透的第一个关键点。
旧 C++ 里,右值并不是完全"接不住":
cpp
const std::string& rs = std::string("hello");
这在 C++98 就成立。
但问题在于:能绑定右值,不等于能表达资源转移。
const T& 的语义
它更像是在说:
我可以借过来看,但我不会动你。
T&& 的语义
它更像是在说:
你这个对象大概率马上就不用了,我可以考虑接管你的资源。
对比一下会更清楚:
| 写法 | 能绑定右值吗 | 能自然表达资源转移吗 |
|---|---|---|
const T& |
能 | 不能 |
T&& |
能 | 能 |
所以右值引用不是旧能力的重复,而是为"移动语义"提供了正式的语言入口。
这里有一句很值得记住:
右值引用不是为了"绑定右值"而存在的,它是为了让"资源转移"第一次有了语言级表达。
四、贯穿案例:先准备一个资源类
为了把后面的现象讲清楚,我们用一个简化版字符串类做贯穿案例:
cpp
#include <cstring>
#include <iostream>
using namespace std;
class String
{
public:
String(const char* str = "")
: _size(strlen(str))
, _data(new char[_size + 1])
{
strcpy(_data, str);
cout << "构造: " << _data << '\n';
}
String(const String& other)
: _size(other._size)
, _data(new char[_size + 1])
{
strcpy(_data, other._data);
cout << "拷贝构造: " << _data << '\n';
}
String(String&& other) noexcept
: _size(other._size)
, _data(other._data)
{
other._size = 0;
other._data = nullptr;
cout << "移动构造\n";
}
String& operator=(const String& other)
{
cout << "拷贝赋值\n";
if (this != &other)
{
char* tmp = new char[other._size + 1];
strcpy(tmp, other._data);
delete[] _data;
_data = tmp;
_size = other._size;
}
return *this;
}
String& operator=(String&& other) noexcept
{
cout << "移动赋值\n";
if (this != &other)
{
delete[] _data;
_data = other._data;
_size = other._size;
other._data = nullptr;
other._size = 0;
}
return *this;
}
~String()
{
delete[] _data;
}
private:
size_t _size = 0;
char* _data = nullptr;
};
这个类已经足够说明:
- 为什么深拷贝会贵
- 移动到底节省了什么
std::move为什么只是一个信号- 为什么被移动对象要保持安全状态
五、移动语义的本质:复制资源 vs 转移资源
考虑一个典型右值:
cpp
String("hello")
它是临时对象,很快就会销毁。
如果另一个对象现在想获得它的数据,有两种做法:
做法一:拷贝
- 再申请一块内存
- 再复制一遍内容
- 两个对象分别维护资源
做法二:移动
- 直接拿走内部指针
- 让原对象进入安全空状态
- 新对象接管资源
显然,对一个即将销毁的对象来说,第二种更合理。
这就是移动语义的核心:
对即将失效的对象,优先考虑转移资源,而不是复制资源。
一张图看清区别
拷贝构造
#mermaid-svg-yfpSj8GIkjwAiXMm{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-yfpSj8GIkjwAiXMm .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-yfpSj8GIkjwAiXMm .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-yfpSj8GIkjwAiXMm .error-icon{fill:#552222;}#mermaid-svg-yfpSj8GIkjwAiXMm .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-yfpSj8GIkjwAiXMm .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-yfpSj8GIkjwAiXMm .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-yfpSj8GIkjwAiXMm .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-yfpSj8GIkjwAiXMm .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-yfpSj8GIkjwAiXMm .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-yfpSj8GIkjwAiXMm .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-yfpSj8GIkjwAiXMm .marker{fill:#333333;stroke:#333333;}#mermaid-svg-yfpSj8GIkjwAiXMm .marker.cross{stroke:#333333;}#mermaid-svg-yfpSj8GIkjwAiXMm svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-yfpSj8GIkjwAiXMm p{margin:0;}#mermaid-svg-yfpSj8GIkjwAiXMm .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-yfpSj8GIkjwAiXMm .cluster-label text{fill:#333;}#mermaid-svg-yfpSj8GIkjwAiXMm .cluster-label span{color:#333;}#mermaid-svg-yfpSj8GIkjwAiXMm .cluster-label span p{background-color:transparent;}#mermaid-svg-yfpSj8GIkjwAiXMm .label text,#mermaid-svg-yfpSj8GIkjwAiXMm span{fill:#333;color:#333;}#mermaid-svg-yfpSj8GIkjwAiXMm .node rect,#mermaid-svg-yfpSj8GIkjwAiXMm .node circle,#mermaid-svg-yfpSj8GIkjwAiXMm .node ellipse,#mermaid-svg-yfpSj8GIkjwAiXMm .node polygon,#mermaid-svg-yfpSj8GIkjwAiXMm .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-yfpSj8GIkjwAiXMm .rough-node .label text,#mermaid-svg-yfpSj8GIkjwAiXMm .node .label text,#mermaid-svg-yfpSj8GIkjwAiXMm .image-shape .label,#mermaid-svg-yfpSj8GIkjwAiXMm .icon-shape .label{text-anchor:middle;}#mermaid-svg-yfpSj8GIkjwAiXMm .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-yfpSj8GIkjwAiXMm .rough-node .label,#mermaid-svg-yfpSj8GIkjwAiXMm .node .label,#mermaid-svg-yfpSj8GIkjwAiXMm .image-shape .label,#mermaid-svg-yfpSj8GIkjwAiXMm .icon-shape .label{text-align:center;}#mermaid-svg-yfpSj8GIkjwAiXMm .node.clickable{cursor:pointer;}#mermaid-svg-yfpSj8GIkjwAiXMm .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-yfpSj8GIkjwAiXMm .arrowheadPath{fill:#333333;}#mermaid-svg-yfpSj8GIkjwAiXMm .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-yfpSj8GIkjwAiXMm .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-yfpSj8GIkjwAiXMm .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-yfpSj8GIkjwAiXMm .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-yfpSj8GIkjwAiXMm .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-yfpSj8GIkjwAiXMm .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-yfpSj8GIkjwAiXMm .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-yfpSj8GIkjwAiXMm .cluster text{fill:#333;}#mermaid-svg-yfpSj8GIkjwAiXMm .cluster span{color:#333;}#mermaid-svg-yfpSj8GIkjwAiXMm div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-yfpSj8GIkjwAiXMm .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-yfpSj8GIkjwAiXMm rect.text{fill:none;stroke-width:0;}#mermaid-svg-yfpSj8GIkjwAiXMm .icon-shape,#mermaid-svg-yfpSj8GIkjwAiXMm .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-yfpSj8GIkjwAiXMm .icon-shape p,#mermaid-svg-yfpSj8GIkjwAiXMm .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-yfpSj8GIkjwAiXMm .icon-shape .label rect,#mermaid-svg-yfpSj8GIkjwAiXMm .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-yfpSj8GIkjwAiXMm .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-yfpSj8GIkjwAiXMm .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-yfpSj8GIkjwAiXMm :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 源对象 s
资源 hello
目标对象 t
新申请资源 hello
移动构造
#mermaid-svg-ttsMokd16Tf4bWJm{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-ttsMokd16Tf4bWJm .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-ttsMokd16Tf4bWJm .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-ttsMokd16Tf4bWJm .error-icon{fill:#552222;}#mermaid-svg-ttsMokd16Tf4bWJm .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-ttsMokd16Tf4bWJm .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-ttsMokd16Tf4bWJm .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-ttsMokd16Tf4bWJm .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-ttsMokd16Tf4bWJm .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-ttsMokd16Tf4bWJm .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-ttsMokd16Tf4bWJm .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-ttsMokd16Tf4bWJm .marker{fill:#333333;stroke:#333333;}#mermaid-svg-ttsMokd16Tf4bWJm .marker.cross{stroke:#333333;}#mermaid-svg-ttsMokd16Tf4bWJm svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-ttsMokd16Tf4bWJm p{margin:0;}#mermaid-svg-ttsMokd16Tf4bWJm .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-ttsMokd16Tf4bWJm .cluster-label text{fill:#333;}#mermaid-svg-ttsMokd16Tf4bWJm .cluster-label span{color:#333;}#mermaid-svg-ttsMokd16Tf4bWJm .cluster-label span p{background-color:transparent;}#mermaid-svg-ttsMokd16Tf4bWJm .label text,#mermaid-svg-ttsMokd16Tf4bWJm span{fill:#333;color:#333;}#mermaid-svg-ttsMokd16Tf4bWJm .node rect,#mermaid-svg-ttsMokd16Tf4bWJm .node circle,#mermaid-svg-ttsMokd16Tf4bWJm .node ellipse,#mermaid-svg-ttsMokd16Tf4bWJm .node polygon,#mermaid-svg-ttsMokd16Tf4bWJm .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-ttsMokd16Tf4bWJm .rough-node .label text,#mermaid-svg-ttsMokd16Tf4bWJm .node .label text,#mermaid-svg-ttsMokd16Tf4bWJm .image-shape .label,#mermaid-svg-ttsMokd16Tf4bWJm .icon-shape .label{text-anchor:middle;}#mermaid-svg-ttsMokd16Tf4bWJm .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-ttsMokd16Tf4bWJm .rough-node .label,#mermaid-svg-ttsMokd16Tf4bWJm .node .label,#mermaid-svg-ttsMokd16Tf4bWJm .image-shape .label,#mermaid-svg-ttsMokd16Tf4bWJm .icon-shape .label{text-align:center;}#mermaid-svg-ttsMokd16Tf4bWJm .node.clickable{cursor:pointer;}#mermaid-svg-ttsMokd16Tf4bWJm .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-ttsMokd16Tf4bWJm .arrowheadPath{fill:#333333;}#mermaid-svg-ttsMokd16Tf4bWJm .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-ttsMokd16Tf4bWJm .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-ttsMokd16Tf4bWJm .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-ttsMokd16Tf4bWJm .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-ttsMokd16Tf4bWJm .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-ttsMokd16Tf4bWJm .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-ttsMokd16Tf4bWJm .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-ttsMokd16Tf4bWJm .cluster text{fill:#333;}#mermaid-svg-ttsMokd16Tf4bWJm .cluster span{color:#333;}#mermaid-svg-ttsMokd16Tf4bWJm div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-ttsMokd16Tf4bWJm .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-ttsMokd16Tf4bWJm rect.text{fill:none;stroke-width:0;}#mermaid-svg-ttsMokd16Tf4bWJm .icon-shape,#mermaid-svg-ttsMokd16Tf4bWJm .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-ttsMokd16Tf4bWJm .icon-shape p,#mermaid-svg-ttsMokd16Tf4bWJm .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-ttsMokd16Tf4bWJm .icon-shape .label rect,#mermaid-svg-ttsMokd16Tf4bWJm .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-ttsMokd16Tf4bWJm .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-ttsMokd16Tf4bWJm .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-ttsMokd16Tf4bWJm :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 源对象 s
资源 hello
目标对象 t 接管资源
s 变为空/可析构状态
六、移动构造和移动赋值,必须分开讲
这是第二篇里第二个非常关键的点。
1. 移动构造
发生在"用右值初始化新对象"时:
cpp
String s1("hello");
String s2(std::move(s1));
此时 s2 是新对象,可以直接接管 s1 的资源。
2. 移动赋值
发生在"已有对象被右值替换"时:
cpp
String s1("hello");
String s2("world");
s2 = std::move(s1);
这时 s2 已经持有资源了,所以移动赋值要做的事情更多:
- 释放自身旧资源
- 接管新资源
- 保证状态安全
对比一下
| 操作 | 场景 | 重点 |
|---|---|---|
| 移动构造 | 右值初始化新对象 | 直接接管资源 |
| 移动赋值 | 右值替换已有对象 | 先清旧资源,再接管新资源 |
为什么源对象必须保持"可析构状态"
因为被移动后,对象生命周期并没有结束。它之后依然可能:
- 被析构
- 被重新赋值
- 被重新初始化
所以设计良好的移动操作,不能把源对象变成"非法对象",而应该让它变成"有效但值未指定"的对象。
七、实验一:什么时候拷贝,什么时候移动
先看一段非常典型的实验代码:
cpp
String MakeString()
{
String tmp("hello");
return tmp;
}
int main()
{
String s1("abc");
String s2 = s1; // 拷贝构造
String s3 = std::move(s1); // 移动构造
String s4("world");
s4 = s2; // 拷贝赋值
s4 = std::move(s2); // 移动赋值
String s5 = MakeString(); // 可能拷贝省略,也可能移动
}
典型观察结果
| 语句 | 常见行为 |
|---|---|
String s2 = s1; |
拷贝构造 |
String s3 = std::move(s1); |
移动构造 |
s4 = s2; |
拷贝赋值 |
s4 = std::move(s2); |
移动赋值 |
String s5 = MakeString(); |
可能直接优化掉中间步骤 |
这段实验最想让读者看到两件事:
- 左值默认更偏向拷贝
- 右值更适合移动
但最后一行还会引出一个重要问题:为什么有时你明明写了返回对象,却没看到移动构造输出?
这个问题,要靠返回值优化来解释。
八、std::move 到底做了什么
如果这一篇只记一句"钉子话",那就记这一句:
std::move不移动资源,它只把对象转换成可按右值处理的形式。
看代码:
cpp
String s1("hello");
String s2 = std::move(s1);
很多人会把它理解成:"move 把 s1 移给了 s2。"
这个说法不够准确。更准确的理解是:
std::move显式告诉编译器:这个对象可以按右值看待- 后续重载匹配因此有机会进入移动构造或移动赋值
真正执行资源转移的是:
String(String&&)String& operator=(String&&)- 容器或算法内部调用的移动逻辑
所以 move 不是性能魔法,它只是一个信号。
一个很容易踩的坑
如果类型本身没有高效移动能力,那么你写了 std::move,也不代表一定会更快。
九、为什么右值引用变量本身还是左值
这是第二篇最容易把人绕住的地方,但也是后面完美转发的基础。
看代码:
cpp
int&& rr = 10;
很多人会想:rr 的类型是右值引用,所以 rr 就应该是右值。
其实不对。
正确理解
rr的类型 是int&&- 但
rr这个有名字的表达式,本身是左值
所以:
cpp
int&& rr = 10;
int& r1 = rr; // 可以
// int&& r2 = rr; // 不可以
int&& r3 = std::move(rr); // 可以
为什么会这样
因为"类型"和"表达式属性"不是一回事。
| 维度 | 例子 |
|---|---|
| 类型 | int&& |
| 表达式属性 | 左值 / 右值 |
一个对象一旦有名字,语言就默认它后面还可能被继续使用,所以它在表达式中通常按左值处理。
这里如果没讲清,后面的 forward 几乎一定会讲乱。
十、返回值优化、拷贝省略和移动语义是什么关系
看这个函数:
cpp
String Make()
{
String tmp("hello");
return tmp;
}
调用时:
cpp
String s = Make();
很多人以为这里一定会看到"移动构造"。
实际上不一定。
可能发生的几种情况
- 拷贝构造
- 移动构造
- 返回值优化(RVO / NRVO)
- 拷贝省略
现代编译器里,优化很常见
也就是说:
你没看到移动构造输出,不代表移动语义没意义。
很可能是编译器直接把中间对象优化掉了。
正确认识应该是
- 移动语义很重要
- 但它不是唯一优化来源
- 拷贝省略和返回值优化同样关键
- 它们通常和移动语义协同工作
#mermaid-svg-ePT3DLAsC9aZEpmO{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-ePT3DLAsC9aZEpmO .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-ePT3DLAsC9aZEpmO .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-ePT3DLAsC9aZEpmO .error-icon{fill:#552222;}#mermaid-svg-ePT3DLAsC9aZEpmO .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-ePT3DLAsC9aZEpmO .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-ePT3DLAsC9aZEpmO .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-ePT3DLAsC9aZEpmO .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-ePT3DLAsC9aZEpmO .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-ePT3DLAsC9aZEpmO .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-ePT3DLAsC9aZEpmO .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-ePT3DLAsC9aZEpmO .marker{fill:#333333;stroke:#333333;}#mermaid-svg-ePT3DLAsC9aZEpmO .marker.cross{stroke:#333333;}#mermaid-svg-ePT3DLAsC9aZEpmO svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-ePT3DLAsC9aZEpmO p{margin:0;}#mermaid-svg-ePT3DLAsC9aZEpmO .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-ePT3DLAsC9aZEpmO .cluster-label text{fill:#333;}#mermaid-svg-ePT3DLAsC9aZEpmO .cluster-label span{color:#333;}#mermaid-svg-ePT3DLAsC9aZEpmO .cluster-label span p{background-color:transparent;}#mermaid-svg-ePT3DLAsC9aZEpmO .label text,#mermaid-svg-ePT3DLAsC9aZEpmO span{fill:#333;color:#333;}#mermaid-svg-ePT3DLAsC9aZEpmO .node rect,#mermaid-svg-ePT3DLAsC9aZEpmO .node circle,#mermaid-svg-ePT3DLAsC9aZEpmO .node ellipse,#mermaid-svg-ePT3DLAsC9aZEpmO .node polygon,#mermaid-svg-ePT3DLAsC9aZEpmO .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-ePT3DLAsC9aZEpmO .rough-node .label text,#mermaid-svg-ePT3DLAsC9aZEpmO .node .label text,#mermaid-svg-ePT3DLAsC9aZEpmO .image-shape .label,#mermaid-svg-ePT3DLAsC9aZEpmO .icon-shape .label{text-anchor:middle;}#mermaid-svg-ePT3DLAsC9aZEpmO .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-ePT3DLAsC9aZEpmO .rough-node .label,#mermaid-svg-ePT3DLAsC9aZEpmO .node .label,#mermaid-svg-ePT3DLAsC9aZEpmO .image-shape .label,#mermaid-svg-ePT3DLAsC9aZEpmO .icon-shape .label{text-align:center;}#mermaid-svg-ePT3DLAsC9aZEpmO .node.clickable{cursor:pointer;}#mermaid-svg-ePT3DLAsC9aZEpmO .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-ePT3DLAsC9aZEpmO .arrowheadPath{fill:#333333;}#mermaid-svg-ePT3DLAsC9aZEpmO .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-ePT3DLAsC9aZEpmO .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-ePT3DLAsC9aZEpmO .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-ePT3DLAsC9aZEpmO .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-ePT3DLAsC9aZEpmO .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-ePT3DLAsC9aZEpmO .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-ePT3DLAsC9aZEpmO .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-ePT3DLAsC9aZEpmO .cluster text{fill:#333;}#mermaid-svg-ePT3DLAsC9aZEpmO .cluster span{color:#333;}#mermaid-svg-ePT3DLAsC9aZEpmO div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-ePT3DLAsC9aZEpmO .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-ePT3DLAsC9aZEpmO rect.text{fill:none;stroke-width:0;}#mermaid-svg-ePT3DLAsC9aZEpmO .icon-shape,#mermaid-svg-ePT3DLAsC9aZEpmO .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-ePT3DLAsC9aZEpmO .icon-shape p,#mermaid-svg-ePT3DLAsC9aZEpmO .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-ePT3DLAsC9aZEpmO .icon-shape .label rect,#mermaid-svg-ePT3DLAsC9aZEpmO .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-ePT3DLAsC9aZEpmO .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-ePT3DLAsC9aZEpmO .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-ePT3DLAsC9aZEpmO :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 函数内部局部对象
返回值场景
可能移动构造
可能拷贝省略
可能返回值优化
这也是为什么现代 C++ 中"按值返回对象"比很多人想象中自然得多。
十一、模板世界的问题:怎么保留参数原本是左值还是右值
到这里为止,我们讲的还是普通对象流动。
真正更难的问题出现在模板里。
先看一个"看起来没毛病,其实有问题"的包装函数:
cpp
void Fun(int& x) { cout << "左值\n"; }
void Fun(int&& x) { cout << "右值\n"; }
template<class T>
void Wrapper(T&& arg)
{
Fun(arg);
}
调用:
cpp
int a = 10;
Wrapper(a);
Wrapper(20);
很多人以为:
- 传左值时会打印"左值"
- 传右值时会打印"右值"
但第二个其实也会走左值版本。因为 arg 是一个有名字的变量表达式,所以它本身是左值。
这就暴露出模板世界里的核心问题:
包装函数一旦接住参数,怎么把它原本的左值/右值属性保留下来?
这正是完美转发要解决的事情。
十二、完美转发为什么重要
完美转发解决的,不是"写法优雅"这么简单,而是语义保真。
也就是说:
- 左值传进来,继续按左值传出去
- 右值传进来,继续按右值传出去
const属性也尽量保持
一个正确的写法
cpp
void Fun(int& x) { cout << "左值\n"; }
void Fun(const int& x) { cout << "const 左值\n"; }
void Fun(int&& x) { cout << "右值\n"; }
template<class T>
void Wrapper(T&& arg)
{
Fun(std::forward<T>(arg));
}
调用:
cpp
int a = 10;
const int b = 20;
Wrapper(a); // 左值
Wrapper(b); // const 左值
Wrapper(30); // 右值
Wrapper(std::move(a)); // 右值
这就是完美转发的意义:
包装层尽量不要偷偷改变实参原本的值类别。
十三、为什么模板里的 T&& 很特殊
这一节是第二篇最硬核、但也最值得讲清的地方。
普通语境里的 T&&
cpp
String&& s = String("hello");
这里的 T&& 就是普通右值引用。
模板推导里的 T&&
cpp
template<class T>
void Wrapper(T&& arg);
这里的 T&& 不再只是普通右值引用,它会根据实参发生推导。
传左值时
cpp
int a = 10;
Wrapper(a);
此时 T 会推导成 int&,于是:
cpp
T&& -> int& &&
再根据引用折叠规则,最终变成:
cpp
int&
传右值时
cpp
Wrapper(10);
此时 T 推导成 int,于是:
cpp
T&& -> int&&
引用折叠规则
记住一句最实用的就够了:
只要有左值引用参与折叠,结果通常就是左值引用。
更直观一点:
| 原始组合 | 折叠结果 |
|---|---|
T& & |
T& |
T& && |
T& |
T&& & |
T& |
T&& && |
T&& |
所以模板里的 T&& 能同时接左值和右值。
这也是为什么它常被称为转发引用。
十四、std::forward 到底在做什么
如果说:
std::move是"无条件右值化"- 那么
std::forward<T>就是"按推导结果有条件转发"
这是两者最核心的区别。
| 工具 | 作用 |
|---|---|
std::move |
无条件把对象转成右值语义 |
std::forward<T> |
根据 T 的推导结果保留原本值类别 |
为什么包装函数里不能乱用 move
如果你这样写:
cpp
template<class T>
void Wrapper(T&& arg)
{
Fun(std::move(arg));
}
那么无论调用者传左值还是右值,都会被你强行转成右值。
这显然不对。
正确写法
cpp
template<class T>
void Wrapper(T&& arg)
{
Fun(std::forward<T>(arg));
}
这样才能做到:
- 左值进来,左值出去
- 右值进来,右值出去
十五、实验二:move 和 forward 的区别
下面这组实验很适合直接验证现象:
cpp
#include <iostream>
using namespace std;
void Fun(int& x) { cout << "左值\n"; }
void Fun(const int& x) { cout << "const 左值\n"; }
void Fun(int&& x) { cout << "右值\n"; }
template<class T>
void TestMove(T&& arg)
{
Fun(std::move(arg));
}
template<class T>
void TestForward(T&& arg)
{
Fun(std::forward<T>(arg));
}
int main()
{
int a = 10;
const int b = 20;
TestMove(a); // 被强行转成右值
TestForward(a); // 保持左值
TestMove(30); // 右值
TestForward(30); // 右值
TestForward(b); // const 左值
}
这个实验最想说明什么
move是强制右值化forward是条件转发- 包装层想保留调用者语义,应该优先考虑
forward
这也是为什么 forward 是完美转发的核心,而不是 move。
十六、落到 STL:push_back、emplace_back 和转发的关系
讲完移动语义和完美转发,再看 STL 的设计,就会顺很多。
push_back
典型思路是:
cpp
void push_back(const T& x);
void push_back(T&& x);
它接收一个已经存在的对象,再把这个对象放进容器。
emplace_back
典型思路是:
cpp
template<class... Args>
void emplace_back(Args&&... args);
它不是让你先构造好一个对象再塞进去,而是:
直接把构造对象所需的参数交给容器,由容器内部原地构造元素。
为什么这和完美转发直接相关
因为 emplace_back 要把外部传入的参数,尽量原样地传给元素构造函数。
这正是 Args&&... + std::forward<Args>(args)... 的经典场景。
一个小对比
cpp
std::vector<std::pair<std::string, int>> v;
v.push_back(std::make_pair("apple", 1));
v.emplace_back("banana", 2);
第一种更像"先有对象,再交给容器"。
第二种更像"你把构造参数给我,我自己在里面造"。
但一定要提醒一句
emplace_back不是"永远更快"。
如果你本来就已经有一个现成对象,那么 push_back 和 emplace_back 的差异未必大。
真正体现 emplace_back 价值的,是它避免中间对象构造的场景。
十七、这一篇的主线,真正收拢起来是什么
到这里,我们可以把整篇文章压缩成一条清晰主线:
第一层:右值引用
让语言第一次能明确区分:这个对象快没用了,可以考虑转移资源。
第二层:移动语义
让资源类对象在合适场景下,不必深拷贝,而可以直接接管资源。
第三层:完美转发
让模板和标准库封装层,尽量保留调用者原本的左值/右值语义。
也就是说,C++11 不是简单让对象"能移动",而是在系统性地改进:
对象在函数、容器、模板和返回值之间,究竟应该如何更聪明地流动。
十八、这一篇最容易混淆的 8 个点
| 误区 | 正确认识 |
|---|---|
std::move 会移动资源 |
它只是把对象转换成可按右值处理的形式 |
| 有了右值引用就不会再拷贝 | 是否拷贝取决于重载匹配和类型实现 |
const T& 和 T&& 作用一样 |
前者是只读绑定,后者是资源转移入口 |
| 右值引用变量本身是右值 | 有名字的变量表达式通常是左值 |
| 被移动后的对象不能再用 | 通常仍然有效,但值未指定 |
| 返回对象一定会看到移动构造 | 不一定,可能被编译器优化掉 |
std::move 总能提速 |
不一定,要看类型是否真正支持高效移动 |
emplace_back 一定比 push_back 快 |
不一定,要看具体场景 |
十九、实践建议
1. 资源类要认真考虑移动构造和移动赋值
如果类内部持有独占资源,移动语义通常非常值得实现。
2. 别把 std::move 当成性能咒语
它只是显式信号,不是自动提速器。
3. 模板包装层别乱用 move
如果目的是保留调用者原始语义,优先考虑 std::forward。
4. 被移动对象后续尽量只做重新赋值、析构、重新初始化
不要继续依赖它原本的业务值。
5. 看性能时别只盯着"有没有移动构造输出"
还要结合编译器优化、拷贝省略、返回值优化一起判断。
二十、这一篇你真正应该带走的结论
结论 1
右值真正重要的地方,不在于"临时",而在于"它通常快没用了,所以适合转移资源"。
结论 2
右值引用真正重要的意义,是为移动语义提供了正式语言基础。
结论 3
std::move 不移动资源,它只是把对象转换成可按右值处理的形式。
结论 4
移动构造和移动赋值的核心价值,是避免对资源类对象做无意义的深拷贝。
结论 5
完美转发解决的是模板世界里的语义保真问题:尽量保留参数原本的左值/右值属性。
结论 6
第一篇解决"对象怎么更自然地创建",这一篇解决"对象怎么更高效地流动"。
二十一、本文总结
第一篇里,我们讲的是对象如何更自然地出生。
这一篇,我们讲的是对象如何更高效地流动。
右值引用让临时对象第一次拥有了明确的资源语义。
移动语义让资源不再总是被迫复制,而可以在合适的时候被转移。
完美转发则让这种能力真正进入模板和标准库世界。
所以,C++11 的价值从来不只是"多了一些新语法",而是:
它开始让 C++ 真正学会区分,什么时候应该复制,什么时候应该转移,什么时候应该原样保留。
这正是现代 C++ 的第二根支柱。
下篇预告
前两篇我们已经把"对象的出生"和"对象的流动"讲通了。
第三篇就该继续往上走,进入 C++11 更高层的表达能力:
- 可变参数模板
- 参数包展开
emplace- lambda
functionbind