【问题标题】:Why is `std::move` named `std::move`?为什么`std::move`被命名为`std::move`?
【发布时间】:2014-02-16 23:13:31
【问题描述】:

C++11 std::move(x) 函数并没有真正移动任何东西。它只是对 r 值的强制转换。为什么这样做?这不是误导吗?

【问题讨论】:

  • 更糟糕的是,三个参数std::move实际上移动了..
  • 别忘了 C++98/03/11 std::char_traits::move :-)
  • 我最喜欢的另一个是std::remove(),它不会删除元素:您仍然需要调用erase() 才能真正从容器中删除这些元素。所以move 不会移动,remove 不会移除。我会为move 选择名称mark_movable()
  • @Ali 我会发现mark_movable() 也令人困惑。它表明实际上没有持久的副作用。

标签: c++ c++11 move-semantics rvalue-reference c++-faq


【解决方案1】:

std::move(x) 只是对右值的强制转换是正确的 - 更具体地说是对xvalue, as opposed to a prvalue。确实,有一个名为move 的演员有时会让人们感到困惑。然而,这个命名的目的不是为了混淆,而是为了让你的代码更具可读性。

move 的历史可以追溯到the original move proposal in 2002。本文先介绍右值引用,然后展示如何写一个更高效的std::swap

template <class T>
void
swap(T& a, T& b)
{
    T tmp(static_cast<T&&>(a));
    a = static_cast<T&&>(b);
    b = static_cast<T&&>(tmp);
}

人们必须记住,在历史的这一点上,“&amp;&amp;”唯一可能的意思是逻辑与。没有人熟悉右值引用,也不熟悉将左值转换为右值的含义(虽然不像 static_cast&lt;T&gt;(t) 那样制作副本)。所以这段代码的读者自然会想:

我知道swap 应该如何工作(复制到临时然后交换值),但是那些丑陋的演员的目的是什么?!

还请注意,swap 实际上只是各种置换修改算法的替代品。这个讨论很多,比swap大很多。

然后提案引入了语法糖,它将static_cast&lt;T&amp;&amp;&gt; 替换为更易读的东西,传达的不是精确的什么,而是为什么

template <class T>
void
swap(T& a, T& b)
{
    T tmp(move(a));
    a = move(b);
    b = move(tmp);
}

move 只是 static_cast&lt;T&amp;&amp;&gt; 的语法糖,现在的代码很能说明为什么会有这些演员:启用移动语义!

必须明白,在历史的背景下,目前很少有人真正理解右值和移动语义之间的密切联系(尽管论文也试图解释这一点):

当给定右值时,移动语义将自动发挥作用 论据。这是非常安全的,因为从 rvalue 不能被程序的其余部分注意到(其他人没有 对右值的引用以检测差异)。

如果当时swap 是这样呈现的:

template <class T>
void
swap(T& a, T& b)
{
    T tmp(cast_to_rvalue(a));
    a = cast_to_rvalue(b);
    b = cast_to_rvalue(tmp);
}

然后人们会看到并说:

但是你为什么要转换为右值?


要点:

事实上,使用move,从来没有人问过:

但你为什么要搬家?


随着时间的推移和提案的完善,左值和右值的概念被提炼成我们今天拥有的值类别

(图片无耻地从dirkgently盗取)

所以今天,如果我们想让swap 准确地说它在做什么,而不是为什么,它应该看起来更像:

template <class T>
void
swap(T& a, T& b)
{
    T tmp(set_value_category_to_xvalue(a));
    a = set_value_category_to_xvalue(b);
    b = set_value_category_to_xvalue(tmp);
}

每个人都应该问自己的问题是,上面的代码是否比以下代码更具可读性:

template <class T>
void
swap(T& a, T& b)
{
    T tmp(move(a));
    a = move(b);
    b = move(tmp);
}

甚至是原版:

template <class T>
void
swap(T& a, T& b)
{
    T tmp(static_cast<T&&>(a));
    a = static_cast<T&&>(b);
    b = static_cast<T&&>(tmp);
}

无论如何,熟练的 C++ 程序员应该知道,在 move 的幕后,除了演员表之外没有其他事情发生。并且初学者 C++ 程序员,至少使用move,将被告知意图是从 rhs move,而不是从 rhs copy,即使他们不完全了解如何完成。

此外,如果程序员希望以另一个名称使用此功能,std::move 不会垄断此功能,并且在其实现中不涉及不可移植的语言魔法。例如,如果想要编码 set_value_category_to_xvalue,并改用它,那么这样做很简单:

template <class T>
inline
constexpr
typename std::remove_reference<T>::type&&
set_value_category_to_xvalue(T&& t) noexcept
{
    return static_cast<typename std::remove_reference<T>::type&&>(t);
}

在 C++14 中它变得更加简洁:

template <class T>
inline
constexpr
auto&&
set_value_category_to_xvalue(T&& t) noexcept
{
    return static_cast<std::remove_reference_t<T>&&>(t);
}

因此,如果您愿意,请以您认为最好的方式装饰您的 static_cast&lt;T&amp;&amp;&gt;,也许您最终会开发出新的最佳实践(C++ 不断发展)。

那么move 在生成的目标代码方面做了什么?

考虑一下test

void
test(int& i, int& j)
{
    i = j;
}

使用clang++ -std=c++14 test.cpp -O3 -S 编译,生成此目标代码:

__Z4testRiS_:                           ## @_Z4testRiS_
    .cfi_startproc
## BB#0:
    pushq   %rbp
Ltmp0:
    .cfi_def_cfa_offset 16
Ltmp1:
    .cfi_offset %rbp, -16
    movq    %rsp, %rbp
Ltmp2:
    .cfi_def_cfa_register %rbp
    movl    (%rsi), %eax
    movl    %eax, (%rdi)
    popq    %rbp
    retq
    .cfi_endproc

现在如果测试改为:

void
test(int& i, int& j)
{
    i = std::move(j);
}

目标代码完全没有变化。可以将此结果概括为:对于普通可移动对象,std::move 没有影响。

现在让我们看看这个例子:

struct X
{
    X& operator=(const X&);
};

void
test(X& i, X& j)
{
    i = j;
}

这会生成:

__Z4testR1XS0_:                         ## @_Z4testR1XS0_
    .cfi_startproc
## BB#0:
    pushq   %rbp
Ltmp0:
    .cfi_def_cfa_offset 16
Ltmp1:
    .cfi_offset %rbp, -16
    movq    %rsp, %rbp
Ltmp2:
    .cfi_def_cfa_register %rbp
    popq    %rbp
    jmp __ZN1XaSERKS_           ## TAILCALL
    .cfi_endproc

如果您运行 __ZN1XaSERKS_c++filt,它会生成:X::operator=(X const&amp;)。这里没有惊喜。现在如果测试改为:

void
test(X& i, X& j)
{
    i = std::move(j);
}

那么在生成的目标代码中仍然没有任何变化std::move 除了将 j 转换为一个右值之外什么也没做,然后该右值 X 绑定到 X 的复制赋值运算符。

现在让我们添加一个移动赋值运算符到X

struct X
{
    X& operator=(const X&);
    X& operator=(X&&);
};

现在目标代码确实改变了:

__Z4testR1XS0_:                         ## @_Z4testR1XS0_
    .cfi_startproc
## BB#0:
    pushq   %rbp
Ltmp0:
    .cfi_def_cfa_offset 16
Ltmp1:
    .cfi_offset %rbp, -16
    movq    %rsp, %rbp
Ltmp2:
    .cfi_def_cfa_register %rbp
    popq    %rbp
    jmp __ZN1XaSEOS_            ## TAILCALL
    .cfi_endproc

运行__ZN1XaSEOS_c++filt 表明正在调用X::operator=(X&amp;&amp;) 而不是X::operator=(X const&amp;)

这就是std::move 的全部内容!它在运行时完全消失。它唯一的影响是在编译时它可能改变调用的重载。

【讨论】:

  • 这是该图的点源:我为公共利益重新创建了它digraph D { glvalue -&gt; { lvalue; xvalue } rvalue -&gt; { xvalue; prvalue } expression -&gt; { glvalue; rvalue } } :) 下载它here as SVG
  • 这仍然对自行车脱落开放吗?我建议allow_move ;)
  • @dyp 我最喜欢的还是movable
  • Scott Meyers 建议将 std::move 重命名为 rvalue_castyoutube.com/…
  • 既然 rvalue 现在既指 prvalues 又指 xvalues,rvalue_cast 的含义是模棱两可的:它返回什么样的 rvalue? xvalue_cast 在这里是一个一致的名称。不幸的是,此时大多数人也不明白它在做什么。再过几年,我的声明有望成为假。
【解决方案2】:

让我在这里引用 B. Stroustrup 所写的 C++11 FAQ 的一句话,这是对 OP 问题的直接回答:

move(x) 表示“您可以将 x 视为右值”。也许它会有 如果 move() 被称为 rval() 会更好,但现在 move() 已经 多年来一直使用。

顺便说一句,我真的很喜欢这个常见问题解答 - 值得一读。

【讨论】:

  • 从另一个答案抄袭@HowardHinnant 的评论:Stroustrup 的答案不准确,因为现在有两种右值 - 右值和 xvalue,而 std::move 实际上是 xvalue 转换。
猜你喜欢
  • 2012-07-28
  • 2020-12-23
  • 2017-06-11
  • 2021-08-17
  • 2019-07-09
  • 1970-01-01
  • 2016-12-05
  • 1970-01-01
相关资源
最近更新 更多