C ++ 11 std::move(x)
函数实际上根本不会移动任何东西。它只是对r值的转换。为什么要这样做?这不是误导吗?
最佳答案
正确的是,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&&>
的语法糖,现在的代码对于那些强制转换的原因很有启发性:启用移动语义!必须了解,在历史的背景下,目前很少有人真正了解右值与移动语义之间的紧密联系(尽管本文也试图对此进行解释):
给定右值时,Move语义将自动起作用
论点。这是绝对安全的,因为从
程序的其余部分无法注意到右值(其他人没有
为了检测差异而引用右值)。
如果当时
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
的幕后,没有什么比强制转换更重要了。至少使用move
的初学者C ++程序员将被告知,意图是从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
如果通过
__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
的全部!它在运行时完全消失。它的唯一影响是在编译时,在编译时它可能会更改调用重载的方式。
关于c++ - 为什么`std::move`被命名为`std::move`?,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/41346115/