C++嵌套函数调用的命名返回值优化

7
我知道NRVO允许一个函数在没有复制或移动操作的情况下构造并通过值返回该对象。它还可以与嵌套函数调用一起使用,允许您从另一个函数调用的返回值构造对象。
请考虑以下程序及其输出,如注释所示: (来自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情况。同样,该对象只被构造一次,从未被复制或移动。到目前为止都很好。

现在进入我的问题——如何使TestT1TestTM具有相同的效率?正如您在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()

这样做安全吗?
1个回答

2
这是因为在函数参数列表中,编译器明确禁止应用NRVO来自传值参数。在Test1中,您接受一个W实例作为函数参数,因此编译器无法省略返回时的移动操作。

请参见为什么从传值参数中排除NRVO?以及我与Howard Hinnant在评论中讨论的问题为什么for_each返回移动函数
由于这个原因,您不能使Test1像之前那样高效地工作。
标准中的相关引用:
15.8.3 复制/移动省略 [class.copy.elision]
当满足某些条件时,实现允许省略类对象的复制/移动构造...
在具有类返回类型的函数的return语句中,当表达式是非易失性自动对象(不是函数参数或由处理程序(18.3)的异常声明引入的变量),其类型与函数返回类型相同(忽略cv-qualification)时,可以通过直接将自动对象构造到函数调用的返回对象中来省略复制/移动操作。

谢谢,我想我明白了。但是为什么 TestT2 不工作?我以为引用会延长临时对象的生命周期? - Barnett
@Barnett 如果绑定到引用的对象的生命周期只有在被绑定的对象是完整对象(对于大多数目的,如果它是prvalue)或完整对象的完整子对象(例如auto&& val = Something{}.member_variable)时才会延长。没有其生命周期延长的临时对象将持续整个它们出现的表达式。因此,在TestT2中的&&参数和返回值不会延长任何超出函数调用所在表达式的生命周期的临时对象的寿命。 - Curious
还可以参考这里的两个回答https://dev59.com/w5_ha4cB1Zd3GeqPzHdk,它们可能会更好地解释问题。 - Curious
@Curious 可以通过某种方式通过方法传递对象,以保留保证 RVO 保留的方式吗(即避免 prvalue 实例化)? - haelix

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