背景#
move semantics 是 modern cpp 中非常重要的特性,有必要详细了解一下。
update time:2022 年 7 月 2 日
move semantic#
基本的内容大家都很熟悉,就不说了
std::move 做了什么#
std::move 没有 move 任何内容,只是简单地把传进来的参数转换为对应的 rvalue reference。
实现为:
1template<typename T>
2constexpr std::remove_reference_t<T>&& move(T&& t) noexcept
3{
4 return static_cast<std::remove_reference_t<T>&&>(t);
5}被std::move的值处于一个什么状态#
说人话就是,对于 moved from object,仍然可以对其进行一些操作,但是其 state 是未知的。
move 的本质是资源所有权的转移:
我们拆开来看
这里提到的一些操作,其实只有三种:
- 析构函数
- move assignment operator
- copy assignment operator
其余操作都是非法的。
为什么 state 是未知的呢?因为这和实现有关。 标准只要求 moved from object 是可以被析构的,但是内部是什么值,其实是不确定的。 参考 cpp core guidelines C.64
Ideally, that moved-from should be the default value of the type. Ensure that unless there is an exceptionally good reason not to. However, not all types have a default value and for some types establishing the default value can be expensive. The standard requires only that the moved-from object can be destroyed. Often, we can easily and cheaply do better: The standard library assumes that it is possible to assign to a moved-from object. Always leave the moved-from object in some (necessarily specified) valid state.
此外,由于moved from object在出了所在的scope后就会调用析构函数,因此称之为一个 “expiring value(xvalue)",也就是临近生命周期终点的"将亡值”。详细可以参考 浅谈 Cpp Value Categories
when not to use std::move#
最常见的一种情景是,在函数 return 时错误地使用了 std::move,从而阻止了编译器可能的 copy elision。
1T fn()
2{
3 T t;
4 return std::move(t);
5}实际上,编译器会先尝试 copy elision,如果做不到,那么会隐式地做 std::move。
forwarding reference#
其实就是 universal reference,forwarding reference 是官方名称。
1template<typename T>
2void f(T&& x); // forwarding reference
3
4auto && var2 = var1; // forwarding reference
forwarding reference 的规则:
- 被 lvalue 初始化就是 lvalue reference
- 被 rvalue 初始化就是 rvalue reference
forwarding reference 从语法上看起来和rvalue reference 还是比较相像的,那如何区分呢? 如下情况时,实际上是forwarding reference而不是rvalue reference
- 发生了 type deduction
- 从形式上看,出现了
T&&或者auto&&
perfect forwarding#
完美转发,也叫精确传递,指的是“需要将一组参数原封不动的传递给另一个函数”。
原封不动的含义是说:数值不变,左值/右值属性不变,const/non-const属性不变。
在泛型函数中,这样的需求非常普遍。
比如下面这个例子:
1template <typename T> void forward_value(const T& val) {
2 process_value(val);
3}
4template <typename T> void forward_value(T& val) {
5 process_value(val);
6}
7
8int a = 0;
9const int &b = 1;
10forward_value(a); // int&
11forward_value(b); // const int&
12forward_value(2); // int&
在右值引用出现之前,模板的每个参数都必须重载两个类型,总的重载次数是指数级的 orz,警察听了想打人
然而有了右值引用,不管模板有多少个参数,我们只需要定义一次,接受一个右值引用的参数,就能够将所有的参数类型原封不动地传递给目标函数。
1template <typename T> void forward_value(T&& val) {
2 process_value(val);
3}
4
5int a = 0;
6const int &b = 1;
7forward_value(a); // int&
8forward_value(b); // const int&
9forward_value(2); // int&&
那么最后说回 std::move。std::move 的作用就是传入左值,返回右值引用,从而利用右值引用带来的好处(节省资源、实现完美转发等)。
1#include <iostream>
2#include <utility>
3#include <vector>
4#include <string>
5
6int main()
7{
8 std::string str = "Hello";
9 std::vector<std::string> v;
10
11 // 使用 push_back(const T&) 重载,
12 // 表示我们将带来复制 str 的成本
13 v.push_back(str);
14 std::cout << "After copy, str is \"" << str << "\"\n";
15
16 // 使用右值引用 push_back(T&&) 重载,
17 // 表示不复制字符串;而是
18 // str 的内容被移动进 vector
19 // 这个开销比较低,但也意味着 str 现在可能为空。
20 v.push_back(std::move(str));
21 std::cout << "After move, str is \"" << str << "\"\n";
22
23 std::cout << "The contents of the vector are \"" << v[0]
24 << "\", \"" << v[1] << "\"\n";
25}
26After copy, str is "Hello"
27After move, str is ""
28The contents of the vector are "Hello", "Hello"参考资料: