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);
}
人们必须记住,在历史的这一点上,“&&”唯一可能的意思是逻辑与。没有人熟悉右值引用,也不熟悉将左值转换为右值的含义(虽然不像 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&&> 的语法糖,现在的代码很能说明为什么会有这些演员:启用移动语义!
必须明白,在历史的背景下,目前很少有人真正理解右值和移动语义之间的密切联系(尽管论文也试图解释这一点):
当给定右值时,移动语义将自动发挥作用
论据。这是非常安全的,因为从
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<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
如果您运行 __ZN1XaSERKS_ 到 c++filt,它会生成:X::operator=(X const&)。这里没有惊喜。现在如果测试改为:
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&&) 而不是X::operator=(X const&)。
这就是std::move 的全部内容!它在运行时完全消失。它唯一的影响是在编译时它可能改变调用的重载。