c++ - 为什么`std::move`被命名为`std::move`?

标签 c++ c++11 move-semantics rvalue-reference c++-faq

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

最佳答案

正确的是,std::move(x)只是对右值的强制转换-更具体地说,是对xvalue, as opposed to a prvalue的强制转换。确实有一个名为move的演员表有时会使人们感到困惑。但是,这种命名的目的不是要混淆,而是要使您的代码更具可读性。

move的历史可以追溯到the original move proposal in 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时,没有人问过:


  但是,为什么要搬家?




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



(图像从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的初学者C ++程序员将被告知,意图是从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的全部!它在运行时完全消失。它的唯一影响是在编译时,在编译时它可能会更改调用重载的方式。

关于c++ - 为什么`std::move`被命名为`std::move`?,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/41346115/

相关文章:

c++ - CWnd::GetParent 导致异常

c++ - 在 MS-DOS 中使用长参数列表(超过 128 个字符)进行编译

C++ std::enable_if 区分 float 和有符号数?

c++ - 将正在 move 的对象的成员作为参数传递是否安全

c++ - 如何将相互依赖的对象 move 在一起并维护内部引用

c++ - 在 ns 2.34 (Jiazi YI ns 2.29) 上添加 mp-olsr 时出现编译错误

c++ - 从双秒到 std::chrono::steady_clock::duration 的简洁转换?

c++ - std::memory_order_acquire 和 memory_order_release 可以分开使用吗?

c++ - 从 sscanf 中提取数据

c++ - 为什么需要 std::move 来调用 std::vector 的 move 赋值运算符