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

140
std::move(x)函数实际上并没有真正移动任何东西。 它只是将一个左值转换为右值。 为什么要这样做呢? 这难道不会引起误导吗?

更糟糕的是,带有三个参数的 std::move 实际上会移动... - Cubbi
别忘了 C++98/03/11 中的 std::char_traits::move :-) - Howard Hinnant
38
我另一个喜欢的函数是 std::remove(),它并不会移除元素:你仍然需要调用 erase() 函数才能将这些元素从容器中真正移除。所以 move 并没有移动,remove 也没有移除。我原本会选择为 move 命名为 mark_movable() - Ali
4
我也觉得mark_movable()很令人困惑。它暗示着会有一个持久的副作用,但实际上并没有。 - finnw
2个回答

192

正确的说法是,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);
}

无论如何,熟练的C++程序员应该知道,在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);
}

在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

运行 __ZN1XaSEOS_ 通过 c++filt 可以发现调用了 X::operator=(X&&) 而不是 X::operator=(X const&)
而这就是 std::move 的全部作用!它在运行时完全消失。它的唯一影响是在编译时,可能会改变被调用的重载函数。

8
这是该图的源代码:我重新创建了它,digraph D { glvalue -> { lvalue; xvalue } rvalue -> { xvalue; prvalue } expression -> { glvalue; rvalue } } 以方便公众使用 :) 可以**在这里下载为SVG格式**。 - sehe
7
这个问题是否还需要进行琐碎的讨论?我建议使用“allow_move” ;) - dyp
2
@dyp 我最喜欢的仍然是 movable - Daniel Frey
6
Scott Meyers建议将std::move重命名为rvalue_casthttp://www.youtube.com/watch?v=BezbcQIuCsY&feature=player_detailpage#t=335 - nairware
6
由于rvalue现在既指prvalues也指xvalues,因此rvalue_cast的含义是不明确的:它返回哪种类型的rvalue?在这里,xvalue_cast将是一个一致的名称。不幸的是,在现在大部分人也不理解它的作用。再过几年,我的说法希望会变得错误。 - Howard Hinnant
显示剩余13条评论

21

让我引用B. Stroustrup在C++11 FAQ中的一句话,这是对OP问题的直接回答:

move(x)的意思是“你可以将x视为右值”。也许如果move()被称为rval()会更好,但现在move()已经使用了多年。

顺便说一下,我非常喜欢这个FAQ——值得阅读。


2
从@HowardHinnant的另一个答案中抄袭评论:Stroustrup的回答是不准确的,因为现在有两种rvalue- prvalues和xvalues,而std :: move实际上是一个xvalue转换。 - einpoklum

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接