本篇谈一下 std::exchange ,其实还有一个姊妹篇讲 copy-and-swap 手法的,这篇算是接着那篇写的。那篇属于群内分享,没有放到公众号上来,原文请阅读 https://www.cppmore.com/2023/07/23/copy-and-swap-idiom-and-more-tricks/。如果你已经读懂了姊妹篇,那就可以接着阅读本篇内容了。1
What and Why
这次单独说一下 std::exchange
,它是 C++14 <utility>
提供的一个函数模板,实现很简单。 1template<class T, class U = T>
2constexpr // since C++20
3T exchange(T& obj, U&& new_value)
4 noexcept( // since C++23
5 std::is_nothrow_move_constructible<T>::value &&
6 std::is_nothrow_assignable<T&, U>::value
7 )
8{
9 T old_value = std::move(obj);
10 obj = std::forward<U>(new_value);
11 return old_value;
12}
看实现可知其逻辑就是设置新值、返回旧值。但却难以顾名思义,它实际上并不会交换数据,想实现那种效果,得这样写:1y = std::exchange(x, y);
姊妹篇说过,std::exchange
和 i++
的逻辑相同。让我们重新再来看一下自增运算符,一个例子: 1struct S {
2 int val{};
3
4 // prefix increment operator
5 constexpr auto& operator++() {
6 ++val;
7 return *this;
8 }
9
10 // postfix increment operator
11 constexpr auto operator++(int) {
12 auto old_value = *this;
13 ++val;
14 return old_value;
15 }
16};
标准通过 operator++()
和 operator++(int)
来区别前自增和后自增,后自增有一个 int
参数,如果滥用一下,那不就是一个非泛化版的 std::exchange
。 1struct S {
2 // ...
3
4 // postfix increment operator
5 // same as std::exchange
6 constexpr auto operator++(int new_value) {
7 auto old_value = *this;
8 val = new_value;
9 return old_value;
10 }
11};
12
13int main() {
14 S s;
15 auto old_value = s.operator++(5);
16
17 return old_value.val;
18}
因此,完全可以将 std::exchange
理解为是泛化版本的后自增,它能支持任意类型。2
Use Case 1: Implementing move semantics
第一个典型的使用场景就是姊妹篇中所使用的,实现移动语义。 1struct S
2{
3 int n{42}; // default member initializer
4 S() = default;
5
6 S(S&& other) noexcept
7 : n { std::exchange(other.n, 0) }
8 {}
9
10 S& operator=(S&& other) noexcept
11 {
12 // safe for this == &other
13 n = std::exchange(other.n, 0); // move n, while leaving zero in other.n
14 return *this;
15 }
16};
17
18int main() {
19 S s;
20 // s = s; // 1. Error! does not match the move assigment operator
21 s = std::move(s); // 2. OK! explicitly move
22
23 std::cout << s.n << "\n"; // Outputs: 42
24}
为何需要这样写?在姊妹篇中已经讲解清楚了。这里再解释一下 self-assignment check,有些实现可能还会额外检查一下:1S& operator=(S&& other) noexcept
2{
3 if (this != &other)
4 n = std::exchange(other.n, 0); // move n, while leaving zero in other.n
5 return *this;
6}
不检查也是完全安全的,看前面那个示例。自赋值的情况非常罕见,你也无法在不经意间使用,因为你必须得显式写出 std::move
才能实现自赋值。为了一个几乎不可能出现的操作,每次都多做一次检查,实属浪费。做这么一次检查,只是避免在自赋值时做一次无谓的交换,安全性来说都一样。一面是为极其罕见的情况每次都做一次检查,一面是省掉每次的检查,如果真有自赋值,也只是做一次无谓的交换。孰优孰劣,你觉得呢?3
Use Case 2: As helper for delimiting output
第二场景是能够简化格式化输出时的代码,不借助 fmt,平常的写法是这样的: 1int main() {
2 std::vector<int> vec(10);
3 std::ranges::iota(vec, 0);
4
5 std::cout << "[";
6 const char* delim = "";
7 for (auto val : vec) {
8 std::cout << delim;
9 std::cout << val;
10 delim = ", ";
11 }
12
13 // Outputs: [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
14 std::cout << "]\n";
15}
借助 std::exchange
可以简化成这样: 1int main() {
2 std::vector<int> vec(10);
3 std::ranges::iota(vec, 0);
4
5 std::cout << "[";
6 const char* delim = "";
7 for (auto val : vec) {
8 std::cout << std::exchange(delim, ", ") << val;
9 }
10
11 // Outputs: [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
12 std::cout << "]\n";
13}
当然这种简化是有代价的,每次都会交换一下。这里只是抛砖引玉,提及一下类似这种需要使用旧值新值的场景可以考虑使用 std::exchange
。4
Use Case 3: Ensuring the transfer of object ownership
1void transfer_ownership(auto& obj) {
2 [o = std::move(obj)] {}();
3}
功能是通过 transfer_ownership()
里面的 lambda 来转移对象的所有权。1std::vector<int> v1(10);
2std::ranges::iota(v1, 0);
3transfer_ownership(v1);
4// Outputs: []
5fmt::print("v1: {}\n", v1);
1std::optional<int> foo{ 42 };
2transfer_ownership(foo);
3// true
4fmt::print("has_value: {}\n", foo.has_value());
虽然在主流 STL 实现中许多对象移动后都会置空,比如 std::vector
, std::string
, std::function
等等,但是标准并未规定对象移动后一定要置空。因此这种做法并不具备可维护性,当你更换一个类型后,它的所有权可能并没有完全转移。解决方法之一就是使用 copy-and-swap 手法,可以这样实现: 1void transfer_ownership(auto& obj) {
2 std::decay_t<decltype(obj)> tmp{};
3 using std::swap;
4 swap(obj, tmp);
5 [o = std::move(tmp)] {}();
6}
7
8std::optional<int> foo{ 42 };
9transfer_ownership(foo);
10// false
11fmt::print("has_value: {}\n", foo.has_value());
另一个解决之法要更加优雅,就是使用 std::exchange
,实现超级简单:1void transfer_ownership(auto& obj) {
2 [o = std::exchange(obj, {})] {}();
3}
4
5std::optional<int> foo{ 42 };
6transfer_ownership(foo);
7// false
8fmt::print("has_value: {}\n", foo.has_value());
通过 std::exchange
能够避免定义临时变量,它的第二个模板参数类型默认与第一个模板参数相同,所以可以非常简单地直接以 {}
构建。此外,其内部存在一次 move construction 和一次 move assignment,较 std::swap
省去了一次 move,不仅同样保证了安全,代码更优雅、更快速。5
总结
总结一下,当遇到设新值、取旧值的情况下,可以考虑使用 std::exchange
,它往往能够优雅地代替原先的几行代码。这是一个非常小的工具,使用情境并不算多,主要是本文中据说的情境一和情境三,在这些情境下,它能够保证代码安全的同时,使其更精确、快速。