C ++ 11 std::move(x)
函数根本不会移动任何东西。它只是一个转换为r值。为什么这样做?这不是误导吗?
答案 0 :(得分:162)
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&&>
替换为更具可读性的东西,而不是传达 ,而是为什么< / EM>:
template <class T>
void
swap(T& a, T& b)
{
T tmp(move(a));
a = move(b);
b = move(tmp);
}
即。 move
只是static_cast<T&&>
的语法糖,现在代码非常暗示为什么那些强制转换存在:启用移动语义!
人们必须明白,在历史背景下,很少有人真正理解rvalues和移动语义之间的密切联系(尽管论文试图解释这一点):
给定rvalue时,移动语义将自动发挥作用 参数。这是非常安全的,因为从中移动资源 程序的其余部分无法注意到右值(其他人没有 对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 移动,而不是来自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
!它在运行时完全消失。它唯一的影响是在编译时它可能改变调用什么重载。
答案 1 :(得分:19)
让我在这里留下B. Stroustrup撰写的C++11 FAQ引用,这是对OP问题的直接回答:
move(x)表示“您可以将x视为右值”。也许它会 如果move()被称为rval(),但是现在move()有了 已使用多年。
顺便说一句,我非常喜欢常见问题 - 值得一读。