为什么`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
实际上仅仅是各种排列修改algorithm的替身。 这个讨论比swap
要大得多。
然后,提案引入了语法糖 ,它将static_cast<T&&>
replace为更具可读性的内容 ,而不是传达精确的内容 ,而是为什么 :
template <class T> void swap(T& a, T& b) { T tmp(move(a)); a = move(b); b = move(tmp); }
即move
只是static_cast<T&&>
语法糖,现在代码是非常暗示为什么这些演员在那里:启用移动语义!
人们必须明白,在历史的背景下,很less有人真正理解了价值和移动语义之间的密切联系(尽pipe这篇论文也试图解释这一点):
当给定右值参数时,移动语义会自动起作用。 这是非常安全的,因为从右值移动资源不会被程序的其余部分注意到( 没有人为了检测差异而引用右值 )。
如果在这个时候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); }
然后人们会看着那个说:
但是你为什么select右值?
就这样,使用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); }
每个人都应该问自己的问题是,如果上面的代码比或多或less可读:
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 ++程序员,至less在move
,会被告知他们的意图是从rhs中移出 ,而不是从rhs中复制 ,即使他们不知道如何完成。
另外,如果一个程序员想要另一个名字的这个function, std::move
在这个function上就没有垄断,并且在它的实现中没有涉及到非便携语言的魔法。 例如,如果想要编码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 ++不断发展)。
那么在生成目标代码方面做什么呢?
考虑这个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
现在,如果testing更改为:
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&)
。 这里不足为奇 现在,如果testing更改为:
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()已经使用了好几年了。
顺便说一句,我真的很喜欢这个常见问题解答 – 值得一读。