为什么`std :: move`被命名为`std :: move`?


128

C ++ 11 std::move(x)函数根本不会移动任何东西。它只是对r值的转换。为什么这样做?这不是误导吗?


更糟糕的是,这三个参数的std::move实际行动..
Cubbi

并且不要忘了C ++ std::char_traits::move
98/03/11

30
我最喜欢的另一个方法是std::remove()不删除元素:您仍然需要调用erase()从容器中实际删除那些元素。所以move不动,remove不动。我会mark_movable()为取这个名字的move
阿里

4
@Ali我也会感到mark_movable()困惑。这表明存在持久的副作用,而实际上没有副作用。
finnw

Answers:


177

正确的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!它在运行时完全消失。它的唯一影响是在编译时,它可能会更改重载调用的内容。


7
下面是该图中的点源:我重新它digraph D { glvalue -> { lvalue; xvalue } rvalue -> { xvalue; prvalue } expression -> { glvalue; rvalue } }为公众不错:)下载它这里SVG
sehe

7
这仍然可以骑自行车吗?我建议allow_move;)
dyp 2014年

2
@dyp我最喜欢的仍然是movable
Daniel Frey 2014年

6
斯科特迈尔斯建议重新命名std::movervalue_castyoutube.com/...
nairware

6
由于rvalue现在同时引用prvalue和xvalue,rvalue_cast因此含义不明确:返回哪种rvalue? xvalue_cast在这里将是一个一致的名称。不幸的是,目前大多数人还不了解它在做什么。再过几年,我的发言有望成为虚假的。
Howard Hinnant 2014年

20

让我在这里留下B.Stroustrup编写的C ++ 11 FAQ中的引号,它是对OP问题的直接答案:

move(x)表示“您可以将x视为右值”。如果将move()称为rval()可能会更好,但是到目前为止move()已经使用了多年。

顺便说一句,我真的很喜欢FAQ-值得一读。


2
another窃@HowardHinnant的另一个答案是:Stroustrup的答案是不准确的,因为现在有两种右值-prvalue和xvalues,而std :: move实际上是xvalue强制转换。
einpoklum
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.