为什么`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()已经使用了好几年了。

顺便说一句,我真的很喜欢这个常见问题解答 – 值得一读。