std::move(x)
函数实际上并没有真正移动任何东西。
它只是将一个左值转换为右值。
为什么要这样做呢?
这难道不会引起误导吗?std::move(x)
函数实际上并没有真正移动任何东西。
它只是将一个左值转换为右值。
为什么要这样做呢?
这难道不会引起误导吗?正确的说法是,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);
}
&&
"唯一可能意味着逻辑和。没有人熟悉rvalue引用,也没有意识到将lvalue强制转换为rvalue(而不进行像static_cast<T>(t)
那样的复制)的影响。所以阅读此代码的人自然会想:swap
应该如何工作(复制到临时变量,然后交换值),但这些丑陋的转换有什么用?!”swap
实际上只是各种置换修改算法的替代品。这个讨论比swap
要大得多。static_cast<T&&>
,它传达的不是精确的what,而是why:template <class T>
void
swap(T& a, T& b)
{
T tmp(move(a));
a = move(b);
b = move(tmp);
}
即move
只是static_cast<T&&>
的语法糖,现在代码已经很说明为什么需要这些转换了:为了启用移动语义!
必须理解的是,在历史的背景下,此时很少有人真正理解rvalues和移动语义之间的密切联系(尽管论文也试图解释):
当提供rvalue参数时,将自动使用移动语义。这是完全安全的,因为从rvalue中移动资源不会被程序的其余部分注意到(没有其他人引用rvalue以检测差异)。
如果在当时将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);
}
move
下,底层只是进行了一个转换。对于初学者而言,至少在使用move
时,他们会明确知道其意图是从右侧进行移动,而不是从右侧进行复制,即使他们并不完全理解如何实现这一点。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);
}
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
__ZN1XaSEOS_
通过 c++filt
可以发现调用了 X::operator=(X&&)
而不是 X::operator=(X const&)
。std::move
的全部作用!它在运行时完全消失。它的唯一影响是在编译时,可能会改变被调用的重载函数。digraph D { glvalue -> { lvalue; xvalue } rvalue -> { xvalue; prvalue } expression -> { glvalue; rvalue } }
以方便公众使用 :) 可以**在这里下载为SVG格式**。 - sehemovable
。 - Daniel Freystd::move
重命名为rvalue_cast
:http://www.youtube.com/watch?v=BezbcQIuCsY&feature=player_detailpage#t=335 - nairwarervalue_cast
的含义是不明确的:它返回哪种类型的rvalue?在这里,xvalue_cast
将是一个一致的名称。不幸的是,在现在大部分人也不理解它的作用。再过几年,我的说法希望会变得错误。 - Howard Hinnant让我引用B. Stroustrup在C++11 FAQ中的一句话,这是对OP问题的直接回答:
move(x)的意思是“你可以将x视为右值”。也许如果move()被称为rval()会更好,但现在move()已经使用了多年。
顺便说一下,我非常喜欢这个FAQ——值得阅读。
std::move
实际上会移动... - Cubbistd::char_traits::move
:-) - Howard Hinnantstd::remove()
,它并不会移除元素:你仍然需要调用erase()
函数才能将这些元素从容器中真正移除。所以move
并没有移动,remove
也没有移除。我原本会选择为move
命名为mark_movable()
。 - Alimark_movable()
很令人困惑。它暗示着会有一个持久的副作用,但实际上并没有。 - finnw