【问题标题】:C++ Named Return Value Optimization with nested function calls具有嵌套函数调用的 C++ 命名返回值优化
【发布时间】:2017-06-10 17:13:28
【问题描述】:

我知道 NRVO 允许函数构造一个对象并按值返回该对象,而无需复制甚至移动操作。它发现它也适用于嵌套函数调用,允许您从另一个函数调用的返回值构造对象。

请考虑以下程序,它的输出如 cmets 所示:
(Visual Studio 2017 版本 15.2 的输出,发布版本。)

#include <stdio.h>
class W
{
public:
  W() { printf( "W::W()\n" ); }
  W( const W& ) { printf( "W::W( const W& )\n" ); }
  W( W&& ) { printf( "W::W( W&& )\n" ); }
  W& operator=( const W& ) { printf( "W::operator=( const W& )\n" ); }
  W& operator=( W&& ) { printf( "W::operator=( W&& )\n" ); }
  ~W() { printf( "W::~W()\n" ); }
  void Transform() { printf( "W::Transform()\n" ); }
  void Run() { printf( "W::Run()\n" ); }
};

W make()
{
  W w;
  return w;
}

W transform_make()
{
  W w{ make() };
  w.Transform();
  return w;
}

W transform1( W w )
{
  w.Transform();
  return w;
}

W&& transform2( W&& w )
{
  w.Transform();
  return std::move(w);
}

int main()                         // Program output:
{
  printf( "TestM:\n" );            //TestM:
  {                                //W::W()
    W w{ make() };                 //W::Run()
    w.Run();                       //W::~W()
  }
                                   //TestTM:
  printf( "TestTM:\n" );           //W::W()
  {                                //W::Transform()
    W w{ transform_make() };       //W::Run()
    w.Run();                       //W::~W()
  }
                                   //TestT1:
  printf( "TestT1:\n" );           //W::W()
  {                                //W::Transform()
    W w{ transform1( make() ) };   //W::W( W&& )
    w.Run();                       //W::~W()
  }                                //W::Run()
                                   //W::~W()

  printf( "TestT2:\n" );           //TestT2:
  {                                //W::W()
    W&& w{ transform2( make() ) }; //W::Transform()
    w.Run();                       //W::~W()
  }                                //W::Run()
}

TestM 是正常的 NRVO 情况。对象W 只被构造和销毁一次。 TestTM 是嵌套的 NRVO 案例。同样,该对象仅被构造一次,并且从未被复制或移动。到目前为止一切顺利。

现在开始我的问题 - 我怎样才能使TestT1 以与TestTM 相同的效率工作?正如您在TestT1 中看到的那样,移动构造了第二个对象——这是我想避免的。如何更改函数transform1() 以避免任何额外的复制或移动?仔细想想,TestT1TestTM 差别不大,所以我有一种感觉,这一定是可能的。

对于我的第二次尝试,TestT2,我尝试通过 RValue 引用传递对象。这消除了额外的移动构造函数,但不幸的是,这会导致在我完成对象之前调用析构函数,这并不总是理想的。

更新:
我还注意到,只要确保不在语句末尾使用对象,就可以使用引用使其工作:

W&& transform2( W&& w )
{
  w.Transform();
  return std::move(w);
}

void run( W&& w )
{
  w.Run();
}

printf( "TestT3:\n" );           //TestT3:
{                                //W::W()
  run( transform2( make() ) );   //W::Transform()
}                                //W::Run()
                                 //W::~W()

这样做安全吗?

【问题讨论】:

    标签: c++ c++11


    【解决方案1】:

    这发生在Test1 中,因为编译器被明确禁止从函数参数列表中的按值参数应用 NRVO。而在Test1 中,您将按值接受W 实例作为函数参数,因此编译器无法忽略返回时的移动。

    请参阅 Why are by-value parameters excluded from NRVO? 以及我与 Howard Hinnant 就该问题在 cmets 中 Why does for_each return function by move 的讨论

    因此,您无法使Test1 像在之前的案例中那样高效地工作。


    标准中的相关引用

    15.8.3 复制/移动省略 [class.copy.elision]

    1. 当满足某些条件时,允许实现省略类对象的复制/移动构造,...

      • 在具有类返回类型的函数中的 return 语句中,当 表达式 是非易失性自动对象的名称(函数参数或由handler (18.3) 的 exception-declaration 与函数返回类型具有相同类型(忽略 cv-qualification),可以通过构造省略复制/移动操作自动对象直接进入函数调用的返回对象

    【讨论】:

    • 谢谢,我想我明白了。但是为什么TestT2 不起作用?我认为引用会延长临时对象的生命周期?
    • @Barnett 绑定到引用的对象的生命周期仅在被绑定的对象是完整对象(对于大多数目的,如果它是纯右值)或完整对象的完整子对象时才会延长对象(例如auto&amp;&amp; val = Something{}.member_variable)。并且没有延长其生命周期的临时变量在它们出现的整个表达式中最后。因此,&amp;&amp; 参数和 TestT2 中的返回值不会延长任何超出表达式生命周期的临时变量的生命周期函数调用出现在
    • 也可以在这里查看两个答案stackoverflow.com/questions/42441791/…,它们可能有助于解释更多内容
    • @Curious 能否以某种方式通过方法传递对象,例如保证 RVO 被保留(即避免纯右值实现)?
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2015-01-15
    • 2012-06-14
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多