【问题标题】:what's the magic of std:movestd:move 的魔力是什么
【发布时间】:2023-03-29 17:59:02
【问题描述】:

以下代码使VC2010失败:

//code1
std::string& test1(std::string&& x){
  return x;
}
std::string str("xxx");
test1(str);  //#1 You cannot bind an lvalue to an rvalue reference

//code2 
std::string&& test1(std::string&& x){
  return x;  //#2 You cannot bind an lvalue to an rvalue reference
}

有一些文章解释#1,但我不明白为什么#2也失败了。

让我们看看 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);
    }
  1. move的参数仍然是右值引用,但是move(str)是可以的!
  2. move 也返回右值。

std:move 有什么魔力?

谢谢

【问题讨论】:

标签: c++ c++11 rvalue-reference


【解决方案1】:

std::move 的参数看起来像是一个右值引用,这看起来确实令人困惑——当str 不是一个右值时,为什么你可以调用move(str)

这里的技巧是右值引用在模板参数上的工作令人困惑:

如果模板参数Tint,那么T&amp;&amp; 将是一个右值引用int&amp;&amp;
但如果T 是左值引用int&amp;,那么T&amp;&amp; 也将是左值引用int&amp;

这是因为&amp;&amp;&amp; 结合的方式:

Type & &&   ==  Type &
Type && &   ==  Type &
Type & &    ==  Type &
Type && &&  ==  Type &&

所以当你调用move(str)时,Tstd::string&amp;move&lt;std::string&amp;&gt;的参数类型也是std::string&amp;——一个左值引用,它允许函数调用编译。然后move 所要做的就是将值转换为右值引用。

【讨论】:

  • +1 啊,你更好地发现了 OP 的困惑。 move的参数类型确实不是右值引用,而是通用引用
【解决方案2】:

您可以将std::move 视为只是一个转换(但一个表达式 转换,而不是一个类型转换)。表达式std::move(x) 是一个与x 具有相同值的右值,但即使x 本身是一个左值,它也可以工作。

在您的示例“code2”中,x 确实是一个左值(类型为“对字符串的右值引用”)。此左值无法绑定到函数的返回类型(“对字符串的右值引用”),因此您需要将其显式转换为右值表达式。

我们也可以将move 的反义词称为stay,它将右值转换为左值(小心使用!):

template <typename T> T & stay(T && t) { return t; }

这主要用于perverse one-liners,或在酒吧给女孩留下深刻印象。

【讨论】:

    【解决方案3】:

    这里的人已经回答了这个问题,但我觉得需要更明确地说出来。人们经常与右值引用混淆的原因在于规则:

    命名的右值引用是一个左值

    起初令人困惑,这条规则是有道理的。右值引用的目标是绑定到您不再需要的对象:无论是临时对象还是您知道永远不需要的对象,但编译器无法弄清楚。

    命名的右值引用是您可以多次引用的东西:

    std::unique_ptr<int> && rref = std::unique_ptr<int>{ new int{1} };
    std::unique_ptr<int> p2{rref};  // if it worked...
    rref->use();                    // this would crash 
    

    在这里,我在第一行创建了一个临时对象,但由于绑定到右值引用,我使它几乎像一个自动对象一样工作:我可以多次访问它。为了让最后一行工作,第二行不能编译。

    std::move 所做的是将命名的右值引用(左值)更改为未命名的右值引用(右值)。

    【讨论】:

      【解决方案4】:

      有一些文章解释#1,但我不明白为什么#2 也失败了。

      记住,如果它有名字,它就是一个左值。 在第二种情况下,x 仍然是 左值,即使它是 右值引用std::move 能够将 左值 转换为 右值,只需对它们执行 static_cast&lt;Type&amp;&amp;&gt;(yourVar) 即可。结果表达式是一个右值,任何请求右值引用的代码都可以接受它。 (Type &amp;&amp;

      我将用几个例子来说明。在您的原始示例中,替换:

      std::string str("xxx");
      test1(str);
      

      test1(string("xxx"));
      

      在那里,字符串对象不再有名称,它现在是一个右值并被 test1 接受。

      移动是如何工作的?同样,很简单。同样,将您的电话替换为:

      test1(std::move(str));
      

      或与

      test1(static_cast<std::string&&>(str));
      

      基本上相同,只是 std::move 更好地解释了意图。

      要点

      • 如果它有一个名字,它就是一个左值

      • 如果它没有名称,则它是一个右值,并且可以在任何需要 Type &amp;&amp;右值引用)的地方被接受。

      • 你可以 static_cast 一个 lvalue 到一个 rvalue (但使用 std::move 代替,这就是它的用途),但你需要知道什么当你这样做时,你正在这样做。 (也就是说,在实现了move语义的类型上使用它,这里我不会详细介绍,我只会链接到move semantics topic上的一篇很棒的文章)

      【讨论】:

        【解决方案5】:

        移动语义的魔力在于利用可破坏性。

        想象一个管理像 std::string 这样的动态分配缓冲区的生命周期的类: 采用string const&amp; 的复制构造函数无法知道之后是否实际需要该参数,因此必须始终克隆此缓冲区,这可能是一项昂贵的操作。

        如果我们可以(通过重载 string&amp;&amp; 的复制构造函数)知道参数是一次性的,我们就可以“窃取”缓冲区而不必复制(对于复杂的表达式,一遍又一遍)。

        如前所述,右值引用是左值(在上面的示例中,它允许我们完全访问以窃取缓冲区)。现在假设我们想在 另一个类似的重载调用表达式:没有std::move,参数对我们来说似乎很宝贵,string const&amp; 重载得到解决,而std::move 允许我们 重复“移动技巧”(因为我们知道参数来自调用站点的右值)。

        【讨论】:

          猜你喜欢
          • 2016-09-23
          • 2015-09-09
          • 2012-05-15
          • 2012-11-30
          • 2011-04-02
          • 1970-01-01
          • 1970-01-01
          • 2010-09-24
          • 1970-01-01
          相关资源
          最近更新 更多