为什么`std :: move`命名为`std :: move`?
C ++ 11 std::move(x)
函数根本不会移动任何东西。 这只是一个演员r值。 为什么这样做? 这不是误导?
std::move(x)
只是对右值的强制转换是正确的 - 更具体地说,是一个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&&>
语法糖,现在代码对于为什么这些类型在那里是非常有启发性的:启用移动语义!
我们必须明白,在历史背景下,很少有人真正理解rvalues和移动语义之间的密切联系(尽管该论文也试图解释这一点):
给定右值参数时,移动语义会自动起作用。 这是非常安全的,因为从右值移动资源不会被程序的其余部分注意到(没有人为了检测差异而引用右值)。
如果在这个时候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中复制,即使他们不知道如何完成。
另外,如果一个程序员想要另一个名字的这个功能, 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
到右值,然后是右值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
通过c++filt
运行__ZN1XaSEOS_
显示X::operator=(X&&)
被调用,而不是X::operator=(X const&)
。
这就是所有std::move
! 它在运行时完全消失。 它唯一的影响是在编译时它可能会改变重载被调用的地方。
让我在这里留下来自B Stroustrup编写的C ++ 11 FAQ的报价,这是对OP的问题的直接回答:
move(x)的意思是“你可以把x当作右值”。 如果move()被称为rval(),也许会更好,但现在move()已经使用了好几年了。
顺便说一句,我非常喜欢这个常见问题解答 - 值得一读。
链接地址: http://www.djcxy.com/p/73009.html上一篇: Why is `std::move` named `std::move`?
下一篇: Is it better in C++ to pass by value or pass by constant reference?