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

C ++ 11 std::move(x)函数根本不会移动任何东西。 这只是一个演员r值。 为什么这样做? 这不是误导?


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);
}

人们必须回顾,在历史的这一点上,“ && ”可能意味着唯一的事情是合乎逻辑的。 没有人熟悉右值引用,也不知道将左值赋给右值(而不是像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&&>语法糖,现在代码对于为什么这些类型在那里是非常有启发性的:启用移动语义!

我们必须明白,在历史背景下,很少有人真正理解rvalues和移动语义之间的密切联系(尽管该论文也试图解释这一点):

给定右值参数时,移动语义会自动起作用。 这是非常安全的,因为从右值移动资源不会被程序的其余部分注意到(没有人为了检测差异而引用右值)。

如果在这个时候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的引擎下,没有比演员更多的事情了。 初学者的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

如果通过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

通过c++filt运行__ZN1XaSEOS_显示X::operator=(X&&)被调用,而不是X::operator=(X const&)

这就是所有std::move ! 它在运行时完全消失。 它唯一的影响是在编译时它可能会改变重载被调用的地方。


让我在这里留下来自B Stroustrup编写的C ++ 11 FAQ的报价,这是对OP的问题的直接回答:

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

顺便说一句,我非常喜欢这个常见问题解答 - 值得一读。

链接地址: http://www.djcxy.com/p/73009.html

上一篇: Why is `std::move` named `std::move`?

下一篇: Is it better in C++ to pass by value or pass by constant reference?