这一块的内容主要设计:返回值优化,以及参数传递时候的优化。
避免写成return std::move(Myclass())的低质量代码。
0、测试代码
c
#include <iostream>
#include <string>
#include <vector>
using namespace std;
class LoudBot {
public:
std::string _name;
// 1. 普通构造
LoudBot(std::string name) : _name(name) {
std::cout << " [Construct] " << _name << " @" << this << std::endl;
}
// 2. 拷贝构造
LoudBot(const LoudBot& other) : _name(other._name) {
std::cout << " [Copy] from " << other._name << " @" << this << std::endl;
}
// 3. 移动构造
LoudBot(LoudBot&& other) noexcept : _name(std::move(other._name)) {
std::cout << " [Move] from " << _name << " @" << this << std::endl;
}
// 4. 析构
~LoudBot() {
std::cout << " [Destruct] " << _name << " @" << this << std::endl;
}
};
// 场景 A: URVO (Unnamed RVO) - 返回无名临时对象
LoudBot getUnnamed() {
return LoudBot("UnnamedBot");
}
// 场景 B: NRVO (Named RVO) - 返回具名局部变量
LoudBot getNamed() {
LoudBot b("NamedBot");
return b;
}
// 场景 C: 反面教材 - 手动 move 局部变量 (阻止了 RVO)
LoudBot getMoved() {
LoudBot b("MovedBot");
return std::move(b); // 千万别这么干!
}
template<typename T>
void func_for_x(T param){
cout<<"func_for_x"<<endl;
}
template<typename T>
void func_for_cx(const T param){
cout<<"func_for_cx"<<endl;
}
template<typename T>
void func_for_rx(const T& param){
cout<<"func_for_rx"<<endl;
}
void test1(){
//测试形参传递优化
cout<<"---1---"<<endl;
func_for_x(LoudBot("WF"));
cout<<"---2---"<<endl;
func_for_cx(LoudBot("WF"));
cout<<"---3---"<<endl;
func_for_rx(LoudBot("WF"));
LoudBot val("WF");
cout<<"---4---"<<endl;
func_for_x(val);
cout<<"---5---"<<endl;
func_for_cx(val);
cout<<"---6---"<<endl;
func_for_rx(val);
}
void test2(){
// 测试返回值优化
std::cout << "--- 1. Testing URVO (getUnnamed) ---" << std::endl;
LoudBot bot1 = getUnnamed();
std::cout << "\n--- 2. Testing NRVO (getNamed) ---" << std::endl;
LoudBot bot2 = getNamed();
std::cout << "\n--- 3. Testing Pessimizing Move (getMoved) ---" << std::endl;
LoudBot bot3 = getMoved();
std::cout << "\n--- End of Test2 ---" << std::endl;
return;
}
int main(){
test1();
test2();
return 0;
}
开启禁止优化:-fno-elide-constructors 会发现:
sql
---1---
[Construct] WF @0x7ffee7238d00
[Move] from WF @0x7ffee7238d20
func_for_x
[Destruct] WF @0x7ffee7238d20
[Destruct] @0x7ffee7238d00
---2---
[Construct] WF @0x7ffee7238d00
[Move] from WF @0x7ffee7238d20
func_for_cx
[Destruct] WF @0x7ffee7238d20
[Destruct] @0x7ffee7238d00
---3---
[Construct] WF @0x7ffee7238d20
func_for_rx
[Destruct] WF @0x7ffee7238d20
[Construct] WF @0x7ffee7238ce0
---4---
[Copy] from WF @0x7ffee7238d20
func_for_x
[Destruct] WF @0x7ffee7238d20
---5---
[Copy] from WF @0x7ffee7238d20
func_for_cx
[Destruct] WF @0x7ffee7238d20
---6---
func_for_rx
[Destruct] WF @0x7ffee7238ce0
默认编译:会启动优化
ruby
xvxing@xvxing:~/study/algorithm/CF$ g++ -std=c++14 test.cpp -o c14_default
xvxing@xvxing:~/study/algorithm/CF$ ./c14_default
---1---
[Construct] WF @0x7ffdfa1b95e0
func_for_x
[Destruct] WF @0x7ffdfa1b95e0
---2---
[Construct] WF @0x7ffdfa1b95e0
func_for_cx
[Destruct] WF @0x7ffdfa1b95e0
---3---
[Construct] WF @0x7ffdfa1b95e0
func_for_rx
[Destruct] WF @0x7ffdfa1b95e0
[Construct] WF @0x7ffdfa1b95c0
---4---
[Copy] from WF @0x7ffdfa1b95e0
func_for_x
[Destruct] WF @0x7ffdfa1b95e0
---5---
[Copy] from WF @0x7ffdfa1b95e0
func_for_cx
[Destruct] WF @0x7ffdfa1b95e0
---6---
func_for_rx
[Destruct] WF @0x7ffdfa1b95c0
下面介绍的是核心操作返回值优化,相比较上面的传递形参,会多一个知识点:返回临时对象槽
1、返回值优化
核心总结
| 术语 | 全称 | 含义 | C++17 前状态 | C++17 后状态 |
|---|---|---|---|---|
| URVO | Unnamed RVO | 返回无名临时对象 | 编译器优化 (可被禁用) | 语言特性 (强制执行,不可禁用) |
| NRVO | Named RVO | 返回具名局部变量 | 编译器优化 (可被禁用) | 编译器优化 (依然可被禁用) |
| GCE | Guaranteed Copy Elision | 强制复制消除 | 不存在 | 标准强制规定 (针对纯右值) |
知识点
1:参数传递中的临时对象
-
当你调用
func(BigObj())时,BigObj()这个表达式直接在func的形参栈内存地址上进行构造。 -
这不涉及"先构造一个临时对象,再 Move 进形参"。
-
例外:除非该参数是按值传递且 ABI(二进制接口)强制要求通过寄存器传递小对象,否则在逻辑上就是一次直接构造。
2:返回值的演变
-
结论 :C++17 彻底消灭了 Prvalue(纯右值)返回值的"临时槽"。
-
C++14 及以前:
return BigObj();逻辑上是:构造临时对象->(可能 Move/Copy)到返回值槽->析构临时对象 ->(可能 Move/Copy)到接收端变量 -> 析构返回对象槽中的内容。- 虽然编译器会进行优化(RVO),但这只是"优化"。如果使用了
-fno-elide-constructors,你会看到拷贝/移动构造函数的调用。
-
C++17 及以后:
return BigObj();不再创建临时对象。BigObj()被视为一个"初始化指令",直接在接收端的内存地址(Result Slot)上执行构造。- 关键点 :这不再是优化,而是语法规则。即使加上
-fno-elide-constructors,也不会产生拷贝或移动。
3:无名临时对象 (URVO)
-
正解 :
return BigObj();是最高效的(C++17 下 0 Copy, 0 Move)。 -
反面教材 :
return std::move(BigObj());或return std::move(temp);- 原因 :
std::move强制将一个表达式转为 xvalue(将亡值)。 - 后果 :这破坏了"直接构造"的上下文。编译器被迫认为你要"移动"一个东西,因此它必须先创建一个临时对象,然后调用
Move Constructor。 - 代价:多一次移动构造 + 多一次析构。
- 原因 :
4:具名对象 (NRVO) 的特殊性
- C++17 对于无名临时对象,即使手动禁止拷贝擦除,也会强制在实际空间构造
- 对于 NRVO (
LoudBot b; return b;),C++17 并没有 将其升级为强制标准。可以通过-fno-elide-constructors禁止优化。
场景行为解析 (配合代码)
场景 A: return LoudBot("Unnamed"); (URVO)
- C++14 (默认) : 1次构造 (优化掉了拷贝)。
- C++14 (关优化) : 1次构造 + 1次 Move + 1次 Destruct (临时对象)。
- C++17 (无论开关) : 1次构造。 (直接在接收端构造)。
场景 B: LoudBot b; return b; (NRVO)
-
C++17 (默认) : 1次构造 (智能优化)。
-
C++17 (关优化) : 1次构造 + 1次 Move + 1次 Destruct (局部变量 b 死掉了)。
- 注:此处证明 NRVO 不是强制的。
场景 C: LoudBot b; return std::move(b); (反面教材)
-
行为 : 1次构造 + 1次 Move + 1次 Destruct。
-
评价 : 显式
move此时不仅多余,而且是有害的。因为它阻断了编译器进行 NRVO 的可能性(NRVO 要求返回的是变量本身,而不是变量的引用/转换结果)。
实战注意
-
永远不要对返回值使用
std::move:return std::move(local_var);-> 错 (破坏 NRVO)。return local_var;-> 对 (编译器会自动尝试 NRVO,失败则自动隐式 Move)。
-
能用无名对象就用无名对象:
return LoudBot(args);比LoudBot b(args); return b;更可靠,因为前者在 C++17 下有标准层面的零拷贝保证。
-
参数传递:
void func(LoudBot b)+func(LoudBot("name"))是非常高效的 Sink (接收) 模式,C++17 下零开销。