【C++】std::exchange 原子性 返回值优化RVO
文章目录
- std::exchange
- `std::exchange` 的版本引入与底层原理
- 1. **引入版本**
- 2. **底层原理**
- 核心实现(简化版):
- 典型用法示例:
- 3. **C++11 之前的替代方案**
- 4. 经典应用场景对比
- C++11 风格(推荐):
- C++98 风格:
- 5. 性能对比(GCC -O3)
- 6. 现代 C++ 的演进
- 关键总结
- `std::exchange` 和 `std::swap`
- 示例对比
- 总结
- `std::exchange` 的**原子性**
- 1. **单一对象操作的原子性**
- 示例(普通类型)
- 2. **原子类型的原子性保证**
- 示例(原子类型)
- 3. **对比普通赋值的非原子性**
- 4. **注意:原子性≠线程安全**
- 总结
- 独占指针初始化问题
- cpp14 无优化
- cpp14 优化
- cpp17 无优化
- cpp17优化
- 结论
std::exchange
std::exchange
的版本引入与底层原理
1. 引入版本
std::exchange
是 C++14 引入的标准库工具函数,定义在 <utility>
头文件中。
2. 底层原理
核心实现(简化版):
template <typename T, typename U = T>
T exchange(T& obj, U&& new_value) {T old_value = std::move(obj); // 保存旧值obj = std::forward<U>(new_value); // 赋予新值return old_value; // 返回旧值
}
关键机制:
- 移动语义:用
std::move
避免旧值的拷贝 - 完美转发:用
std::forward
保持新值的值类别(左值/右值) - 强异常安全:若赋值抛出异常,旧值仍有效
典型用法示例:
std::vector<int> v1{1, 2, 3};
std::vector<int> v2 = std::exchange(v1, {4, 5, 6});
// 现在:
// v1 = {4, 5, 6}
// v2 = {1, 2, 3}
3. C++11 之前的替代方案
在 C++98/03 中,需要手动实现类似功能:
template <typename T, typename U>
T legacy_exchange(T& obj, const U& new_value) {T old_value = obj; // 拷贝构造(无移动语义)obj = new_value; // 拷贝赋值return old_value;
}
缺陷:
- 效率低:多一次拷贝操作
- 不支持移动语义
- 无法完美转发右值
4. 经典应用场景对比
C++11 风格(推荐):
// 安全转移资源所有权
std::unique_ptr<Resource> acquire_resource() {static std::unique_ptr<Resource> res = std::make_unique<Resource>();return std::exchange(res, nullptr); // 原子性转移
}
C++98 风格:
// 需要额外临时变量
std::auto_ptr<Resource> acquire_resource() {static std::auto_ptr<Resource> res(new Resource);std::auto_ptr<Resource> tmp = res; // 拷贝(所有权转移)res.reset(); // 显式置空return tmp; // 返回临时变量
}
5. 性能对比(GCC -O3)
假设操作 std::string
对象:
操作 | 指令数(x86-64) | 备注 |
---|---|---|
C++98 手动交换 | 15 | 含冗余拷贝 |
std::exchange | 8 | 完全利用移动语义 |
编译器优化后 | 5 | 部分操作可内联消除 |
6. 现代 C++ 的演进
- C++20 增强:
constexpr
支持(可在编译期使用)constexpr int demo() {int x = 10;return std::exchange(x, 20); // 返回10,x变为20 } static_assert(demo() == 10);
关键总结
维度 | C++98 | C++11(含exchange ) |
---|---|---|
资源转移效率 | 需要拷贝 | 移动语义零开销 |
代码安全性 | 容易遗漏重置操作 | 原子性操作 |
表达能力 | 需3行代码 | 单函数调用 |
适用场景 | 基本类型 | 支持移动语义的所有类型 |
最佳实践:在资源管理(如智能指针、锁等)和状态机实现中优先使用
std::exchange
。
std::exchange
和 std::swap
-
功能不同
std::exchange
:将对象替换为新值,并返回旧值,不要求新值的类型与原对象类型相同(只要能转换)。- 普通交换(如
std::swap
):交换两个对象的值,要求类型完全一致。
-
操作对象数量
std::exchange
:操作单个对象(替换其值)。- 普通交换:操作两个对象(互换值)。
-
返回值
std::exchange
:返回旧值(原对象的值)。- 普通交换:无返回值(或仅返回引用)。
-
应用场景
std::exchange
:适合需要替换值并保留旧值的场景,如初始化智能指针、重置状态等。- 普通交换:适合需要互换两个对象值的场景,如排序算法中的元素交换。
示例对比
std::exchange
的使用:
#include <iostream>
#include <utility>int main() {int a = 10;int old_value = std::exchange(a, 20); // a 被替换为 20,返回旧值 10std::cout << "a: " << a << ", old_value: " << old_value << std::endl; // 输出: a: 20, old_value: 10return 0;
}
普通交换的使用:
#include <iostream>
#include <utility>int main() {int a = 10;int b = 20;std::swap(a, b); // a 和 b 的值互换std::cout << "a: " << a << ", b: " << b << std::endl; // 输出: a: 20, b: 10return 0;
}
总结
std::exchange
是“替换并返回旧值”,适用于需要原子性更新并获取旧值的场景。- 普通交换 是“互换两个对象的值”,适用于需要交换两个对象状态的场景。
std::exchange
的原子性
1. 单一对象操作的原子性
std::exchange
保证对单个对象的修改是不可分割的。具体来说:
- 替换值的过程不会被中断:当
std::exchange
修改一个对象时,其他线程不会看到“部分修改”的中间状态。 - 返回旧值与设置新值是一个整体操作:返回的旧值一定是修改前的完整值,新值也一定是一次性设置完成的。
示例(普通类型)
int a = 10;
int old = std::exchange(a, 20); // 原子性替换 a 的值并返回旧值
这里,a
的值要么是 10(替换前),要么是 20(替换后),不会出现中间状态。
2. 原子类型的原子性保证
当 std::exchange
用于 std::atomic
类型时,它提供硬件级别的原子操作,等价于 std::atomic::exchange
:
示例(原子类型)
#include <atomic>std::atomic<int> atomic_a(10);
int old = std::exchange(atomic_a, 20); // 原子操作,等价于 atomic_a.exchange(20)
此时:
- 线程安全:多个线程同时调用
std::exchange
操作同一个原子变量时,不会出现数据竞争。 - 内存顺序保证:默认使用
std::memory_order_seq_cst
(顺序一致性),可通过std::atomic
的重载版本指定其他内存顺序。
3. 对比普通赋值的非原子性
普通赋值操作不保证原子性,可能被其他线程观察到中间状态:
int a = 10;
int temp = a; // 步骤1:读取旧值
a = 20; // 步骤2:写入新值
// 其他线程可能在步骤1和步骤2之间观察到 a 的值
而 std::exchange
将这两个步骤合并为一个原子操作。
4. 注意:原子性≠线程安全
std::exchange
对单个对象的操作是原子的,但如果多个对象的操作需要原子性(如交换两个变量),仍需额外同步。- 对于非原子类型(如
int
、std::vector
),std::exchange
仅保证操作的不可分割性,但不保证线程安全(多个线程同时访问仍需锁)。
总结
std::exchange
的原子性体现在:
- 对单个对象的修改不可分割,避免中间状态。
- 对
std::atomic
类型提供硬件级原子操作,保证线程安全。
这使得它在资源管理(如智能指针)、状态更新(如标志位翻转)等场景中特别有用,能避免竞态条件和数据不一致问题。
独占指针初始化问题
cpp14 无优化
g++ $^ -o $@ -std=c++14 -fno-elide-constructors
// lhr::unique_ptr<int> ptr1 = func1();
lhr::unique_ptr<int> func1()
{return lhr::unique_ptr<int>(new int(520));
}raw cons // 原始构造,对应 new int(520)
move cons // 从临时对象移到返回值
delete // 临时对象析构
move cons // 返回值移到 ptr1
delete // 返回值析构
delete // ptr1 析构// lhr::unique_ptr<int> ptr2 = func2();
lhr::unique_ptr<int> func2()
{ auto p = lhr::unique_ptr<int>(new int(520));return p;
}
raw cons // 构造p
move cons // p移动到临时对象
delete // p析构
move cons // 临时对象移动返回值
delete //临时对象析构
move cons // 返回值移动 ptr2
delete // 返回值
delete //ptr2
cpp14 优化
g++ $^ -o $@ -std=c++14
// lhr::unique_ptr<int> ptr1 = func1();
lhr::unique_ptr<int> func1()
{return lhr::unique_ptr<int>(new int(520));
}
raw cons //原地构造ptr1
delete// lhr::unique_ptr<int> ptr2 = func2();
lhr::unique_ptr<int> func2()
{ auto p = lhr::unique_ptr<int>(new int(520));return p;
}
raw cons
delete
cpp17 无优化
g++ my.cc -o main -std=c++17 -fno-elide-constructors
// lhr::unique_ptr<int> ptr1 = func1();
lhr::unique_ptr<int> func1()
{return lhr::unique_ptr<int>(new int(520));
}
raw cons
delete// lhr::unique_ptr<int> ptr2 = func2();
lhr::unique_ptr<int> func2()
{ auto p = lhr::unique_ptr<int>(new int(520));return p;
}
raw cons
move cons
delete
delete
cpp17优化
g++ my.cc -o main -std=c++17
// lhr::unique_ptr<int> ptr1 = func1();
lhr::unique_ptr<int> func1()
{return lhr::unique_ptr<int>(new int(520));
}
raw cons
delete// lhr::unique_ptr<int> ptr2 = func2();
lhr::unique_ptr<int> func2()
{ auto p = lhr::unique_ptr<int>(new int(520));return p;
}
raw cons
delete
结论
c++11/14 使用移动构造代替拷贝优化
c++17使用RVO(返回值优化)代替移动构造优化