Answers:
正确的std::move(x)
是,这只是对rvalue的强制转换-更具体地说,是对xvalue的转换,而不是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语义将自动起作用。这是绝对安全的,因为程序的其余部分无法注意到从右值移出资源(没有其他人引用右值以检测差异)。
如果当时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
,没有人问过:
但是你为什么要搬家?
随着岁月的流逝和提案的完善,左值和右值的概念被细化为今天的价值类别:
(图像从暗中偷偷偷偷偷偷地被偷走了)
所以今天,如果我们想要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
如果您运行__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
!它在运行时完全消失。它的唯一影响是在编译时,它可能会更改重载调用的内容。
allow_move
;)
movable
。
rvalue_cast
因此含义不明确:返回哪种rvalue? xvalue_cast
在这里将是一个一致的名称。不幸的是,目前大多数人还不了解它在做什么。再过几年,我的发言有望成为虚假的。
让我在这里留下B.Stroustrup编写的C ++ 11 FAQ中的引号,它是对OP问题的直接答案:
move(x)表示“您可以将x视为右值”。如果将move()称为rval()可能会更好,但是到目前为止move()已经使用了多年。
顺便说一句,我真的很喜欢FAQ-值得一读。
std::move
实际行动..