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

Why is std::move named std::move?

提问人:Howard Hinnant 提问时间:1/26/2014 最后编辑:Jan SchultkeHoward Hinnant 更新时间:9/10/2023 访问量:14576

问:

该函数根本没有真正移动任何东西。 它只是对右值的强制转换。 为什么要这样做? 这不是误导吗?std::move(x)

C 移动语义 语言设计 右值引用 C++常见问题

评论

0赞 Cubbi 1/26/2014
更糟糕的是,这三个论点实际上在移动。std::move
0赞 Howard Hinnant 1/26/2014
不要忘记 C++98/03/11 :-)std::char_traits::move
38赞 Ali 1/26/2014
我最喜欢的另一个是它不删除元素:您仍然必须调用才能从容器中实际删除这些元素。所以不动,不移。我会为 .std::remove()erase()moveremovemark_movable()move
4赞 finnw 6/26/2018
@Ali我也会感到困惑。它表明存在持久的副作用,而实际上没有副作用。mark_movable()

答:

192赞 Howard Hinnant 1/26/2014 #1

正确的是,这只是对 rvalue 的强制转换 - 更具体地说是 xvalue,而不是 prvalue。而且,有名的演员有时会让人们感到困惑,这也是事实。但是,此命名的目的不是混淆,而是使代码更具可读性。std::move(x)move

其历史可以追溯到 2002 年的原始搬迁提案。本文首先介绍了右值引用,然后展示了如何编写一个更高效的:movestd::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

还要注意的是,这实际上只是各种排列修改算法的替代品。这个讨论比 .swapswap

然后,该提案引入了语法糖,它用更具可读性的东西代替了语法糖,这种语法传达的不是确切的内容,而是原因static_cast<T&&>

template <class T>
void
swap(T& a, T& b)
{
    T tmp(move(a));
    a = move(b);
    b = move(tmp);
}

J.F. 只是 的语法糖,现在代码非常有启发性地说明为什么这些强制转换在那里:启用移动语义!movestatic_cast<T&&>

人们必须明白,在历史的语境中,在这一点上,很少有人真正理解重值和移动语义之间的密切联系(尽管本文也试图解释这一点):

当给定 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

但你为什么要搬家?


随着岁月的流逝和提案的完善,lvalue 和 rvalue 的概念被细化为我们今天拥有的价值类别

Taxonomy

(图片无耻地从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++ 程序员应该知道,在引擎盖下,除了演员之外,没有什么比这更重要的事情了。初学者 C++ 程序员,至少会被告知,目的是从 rhs 移动,而不是从 rhs 复制,即使他们不确切了解如何完成。movemove

此外,如果程序员希望以另一个名称使用此功能,则对此功能没有垄断权,并且其实现中不涉及不可移植的语言魔法。例如,如果有人想编码 ,并改用它,那么这样做是微不足道的:std::moveset_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);
}

因此,如果您愿意,请按照您认为最好的方式进行装饰,也许您最终会开发出新的最佳实践(C++ 在不断发展)。static_cast<T&&>

那么,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++filtX::operator=(X const&)

void
test(X& i, X& j)
{
    i = std::move(j);
}

然后,生成的对象代码仍然没有任何变化。 除了转换为右值之外什么也没做,然后该右值绑定到 的复制赋值运算符。std::movejXX

现在让我们添加一个移动赋值运算符: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++filtX::operator=(X&&)X::operator=(X const&)

仅此而已!它在运行时完全消失。它的唯一影响是在编译时,它可能会改变调用的重载。std::move

评论

8赞 sehe 1/26/2014
这是该图的点源:我为公共利益重新创建了它:)在此处下载为 SVG 格式digraph D { glvalue -> { lvalue; xvalue } rvalue -> { xvalue; prvalue } expression -> { glvalue; rvalue } }
7赞 dyp 1/26/2014
这仍然对自行车脱落开放吗?我建议allow_move ;)
2赞 Daniel Frey 1/26/2014
@dyp 我最喜欢的仍然是。movable
6赞 nairware 1/27/2014
斯科特·迈耶斯(Scott Meyers)建议重命名为:youtube.com/...std::movervalue_cast
6赞 Howard Hinnant 1/27/2014
由于右值现在同时指 prvalues 和 xvalues,因此其含义是模棱两可的:它返回什么样的右值? 在这里将是一个一致的名字。不幸的是,大多数人在这个时候也不会明白它在做什么。再过几年,我的说法有望成为错误。rvalue_castxvalue_cast
21赞 podkova 1/26/2014 #2

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

move(x) 表示“您可以将 x 视为 rValue”。也许它会 如果 move() 被称为 rval() 会更好,但现在 move() 已经 已经使用了多年。

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

评论

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