std::move如何将表达式转换为右值?

125

我发现自己并没有完全理解std::move()的逻辑。

一开始,我尝试通过Google搜索相关内容,但貌似只有关于如何使用std::move()的文档,没有关于其结构如何工作的信息。

我的意思是,我知道这是一个模板成员函数,但当我查看VS2010中std::move()的定义时,仍然感到困惑。

std::move()的定义如下。

template<class _Ty> inline
typename tr1::_Remove_reference<_Ty>::_Type&&
    move(_Ty&& _Arg)
    {   // forward _Arg as movable
        return ((typename tr1::_Remove_reference<_Ty>::_Type&&)_Arg);
    }

对我来说首先很奇怪的是参数 (_Ty&& _Arg),因为当我像下面所见一样调用函数时,

// main()
Object obj1;
Object obj2 = std::move(obj1);

它基本上等同于

// std::move()
_Ty&& _Arg = Obj1;

但正如你已经知道的,你不能直接将LValue链接到RValue引用,这使我认为应该是这样。

_Ty&& _Arg = (Object&&)obj1;

然而,这是荒谬的,因为std::move()必须适用于所有值。

所以我猜要完全理解它是如何工作的,我还应该看一下这些结构体。

template<class _Ty>
struct _Remove_reference
{   // remove reference
    typedef _Ty _Type;
};

template<class _Ty>
struct _Remove_reference<_Ty&>
{   // remove reference
    typedef _Ty _Type;
};

template<class _Ty>
struct _Remove_reference<_Ty&&>
{   // remove rvalue reference
    typedef _Ty _Type;
};

很不幸,这仍然很令人困惑,我不明白。

我知道这都是因为我缺少关于C++基本语法技能的知识。 我想彻底了解它们的工作原理,并且欢迎任何可以在互联网上下载的文档。(如果您可以直接解释这个问题,那将是太棒了。)


34
相反,这个问题本身表明 OP 在那个陈述中低估了自己。正是因为 OP 掌握了基本的语法技能,才使他们能够提出这个问题。 - Kyle Strand
1
无论如何,我不同意 - 即使你能快速学习或已经对计算机科学或整体编程有很好的掌握,甚至在C++中仍然可能会在语法方面遇到困难。最好花时间学习机制、算法等,但需要根据需要依靠参考手册来熟悉语法,而不是技术上表达得准确,但对像复制/移动/转发这样的事情的理解缺乏深度。在了解move的作用之前,你怎么知道"何时以及如何使用std::move进行移动构造"呢? - John P
我认为我来到这里是为了理解move的工作方式而不是它的实现方式。我发现这个解释非常有用:https://pagefault.blog/2018/03/01/common-misconception-with-cpp-move-semantics/. - Anton Daneyko
2个回答

193
我们从move函数开始(我稍微整理了一下):
template <typename T>
typename remove_reference<T>::type&& move(T&& arg)
{
  return static_cast<typename remove_reference<T>::type&&>(arg);
}

让我们从更容易的部分开始 - 即当函数被rvalue调用时:
Object a = std::move(Object());
// Object() is temporary, which is prvalue

我们的move模板实例化如下:
// move with [T = Object]:
remove_reference<Object>::type&& move(Object&& arg)
{
  return static_cast<remove_reference<Object>::type&&>(arg);
}

由于remove_referenceT&转换为TT&&转换为T,而Object不是引用类型,因此我们的最终函数如下:

Object&& move(Object&& arg)
{
  return static_cast<Object&&>(arg);
}

现在,你可能会问:我们需要强制转换吗?答案是:是的,我们需要。原因很简单;命名的右值引用被视为左值(标准禁止将左值隐式转换为右值引用)。
下面是当我们使用左值调用move时会发生什么:
Object a; // a is lvalue
Object b = std::move(a);

和相应的move实例:

// move with [T = Object&]
remove_reference<Object&>::type&& move(Object& && arg)
{
  return static_cast<remove_reference<Object&>::type&&>(arg);
}

同样地,remove_referenceObject&转换为Object,我们得到:

Object&& move(Object& && arg)
{
  return static_cast<Object&&>(arg);
}

现在我们来到了棘手的部分:什么是Object& &&,它是如何与lvalue绑定的?
为了实现完美转发,C++11标准提供了特殊的引用折叠规则,具体如下:
Object &  &  = Object &
Object &  && = Object &
Object && &  = Object &
Object && && = Object &&

正如你所看到的,在这些规则下,Object& &&实际上意味着Object&,这是一个普通的左值引用,允许绑定左值。
因此,最终函数为:
Object&& move(Object& arg)
{
  return static_cast<Object&&>(arg);
}

这与使用rvalue的先前实例化类似——它们都将其参数转换为rvalue引用,然后返回它。区别在于,第一个实例化只能用于rvalue,而第二个实例化可以用于lvalue。


为了更好地解释为什么需要remove_reference,让我们尝试这个函数

template <typename T>
T&& wanna_be_move(T&& arg)
{
  return static_cast<T&&>(arg);
}

使用lvalue实例化它。

// wanna_be_move [with T = Object&]
Object& && wanna_be_move(Object& && arg)
{
  return static_cast<Object& &&>(arg);
}

应用上述参考折叠规则,可以看到我们得到的函数是不可用的,作为move(简单来说,你使用左值调用它,你会得到左值回传)。如果有什么区别,这个函数就是身份函数。

Object& wanna_be_move(Object& arg)
{
  return static_cast<Object&>(arg);
}

4
很好的回答。虽然我理解对于lvalue,将T评估为Object&是一个很好的想法,但我不知道这确实是被执行的。在这种情况下,我原本期望T也会评估为Object,因为我认为这是引入包装器引用和std::ref的原因,或者不是这样吗? - Christian Rau
2
template <typename T> void f(T arg)(这是维基百科文章中的内容)和 template <typename T> void f(T& arg) 之间有所不同。第一个解析为值(如果要传递引用,则必须将其包装在 std::ref 中),而第二个始终解析为引用。不幸的是,模板参数推导规则相当复杂,因此我无法提供精确的理由,说明为什么 T&& 解析为 Object& &&(但确实发生了这种情况)。 - Vitus
1
但是,这种直接的方法是否有任何不起作用的原因呢? template <typename T> T&& also_wanna_be_move(T& arg) { return static_cast<T&&>(arg); } - greggo
1
这就解释了为什么需要remove_reference,但我仍然不明白为什么函数必须采用T&&(而不是T&)。如果我正确理解了这个解释,那么无论你得到T&&还是T&都没有关系,因为无论如何,remove_reference都会将其转换为T,然后你会添加回&&。那么为什么不说你接受一个T&(从语义上讲,这是你接受的),而不是T&&并依靠完美转发来允许调用者传递T&? - mgiuca
4
如果你希望std::move仅将左值转换为右值,那么是的,T&是可以的。这种技巧主要是为了灵活性:你可以对任何东西(包括右值)调用std::move并获取一个右值。 - Vitus
显示剩余7条评论

4
_Ty是一个模板参数,在这种情况下。
Object obj1;
Object obj2 = std::move(obj1);

_Ty是类型"Object &",这就是为什么需要_Remove_reference的原因。

更准确地说,它会像这样

typedef Object& ObjectRef;
Object obj1;
ObjectRef&& obj1_ref = obj1;
Object&& obj2 = (Object&&)obj1_ref;

如果我们不删除引用,那就好像我们在做...
Object&& obj2 = (ObjectRef&&)obj1_ref;

但是ObjectRef&&缩减为Object &,我们无法将其绑定到obj2。

它缩减的原因是为了支持完美转发。请参见这篇论文


这并不能完全解释为什么 _Remove_reference_ 是必要的。例如,如果你有一个 Object& 的 typedef,并且你对它进行引用,你仍然得到的是 Object&。为什么使用 && 不起作用呢?对于这个问题确实有一个答案,它与完美转发有关。 - Nicol Bolas
2
真的。答案非常有趣。A&&&被简化为A&,因此如果我们尝试使用(ObjectRef &&)obj1_ref,则在这种情况下我们将得到Object&。 - Vaughn Cato

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