为什么‘ std: : move’被命名为‘ std: : move’?

C + + 11 std::move(x)函数实际上根本不移动任何东西。它只是 r 值的强制转换。为什么要这么做?这不是误导吗?

13849 次浏览

正确的是,std::move(x)只是一个要升值的强制类型转换——更具体地说,是一个 Xvalue ,与 a < em > prvalue 相反。而且,有一个名为 move的演员阵容有时会让人感到困惑,这也是事实。但是,这个命名的目的不是为了混淆,而是为了使代码更具可读性。

move的历史可以追溯到 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);
}

人们不得不回想起,在历史的这一点上,“ &&”可能意味着的唯一事情是 逻辑和。没有人熟悉右值引用,也没有人知道将左值转换为右值的含义(虽然不像 static_cast<T>(t)那样复制)。因此,这段代码的读者自然会想:

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

还要注意的是,swap实际上只是各种排列修改算法的替身。这个讨论是 很多,比 swap大得多。

然后提案引入了 语法糖,它用一些更易读的东西取代了 static_cast<T&&>,这些东西传达的不是精确的 什么,而是 为什么:

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

也就是说,move只是 static_cast<T&&>的语法糖,现在这段代码对于为什么会有这些强制转换提供了相当的暗示: 启用 move 语义!

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

当给定右值时,移动语义将自动发挥作用 这是完全安全的,因为将资源从 Rvalue 不能被程序的其他部分注意到(< em > 没有其他人注意到 对 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,从来没有人问过:

但你为什么要搬家?


随着时间的推移,提案不断完善,lvalue 和 rvalue 的概念被纳入了我们今天的 价值类别:

Taxonomy

(图像无耻地从 卑鄙地盗取)

所以今天,如果我们想让 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 转向 让开,而不是从 rhs 转向 收到,即使他们不能完全理解已经完成的 怎么做

此外,如果程序员希望使用另一个名称来实现这个功能,那么 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<T&&>,也许您最终将开发一个新的最佳实践(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

如果你通过 c++filt运行 __ZN1XaSERKS_,它会产生: X::operator=(X const&)。没什么好惊讶的。现在,如果测试改为:

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

然后在生成的对象代码中仍然存在 没有任何改变std::move只是将 j强制转换为一个 rvalue,然后该 rvalue X绑定到 X的复制分配操作符。

现在让我们向 X添加一个 move 赋值运算符:

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

通过 c++filt运行 __ZN1XaSEOS_显示正在调用 X::operator=(X&&)而不是 X::operator=(X const&)

那是所有有 std::move!它在运行时完全消失。它唯一的影响是在编译时,在编译时 也许吧改变调用的重载。

让我在这里引用 B. Stroustrup 写的 C + + 11常见问题解答中的一句话,它直接回答了 OP 的问题:

Move (x)的意思是“你可以把 x 看作一个右值” 如果 move ()被称为 rval ()会更好,但是现在 move ()已经 已经用了很多年了。

顺便说一下,我真的很喜欢 FAQ-它值得一读。