使用嵌套函数调用进行 C++ 命名返回值优化

C++ Named Return Value Optimization with nested function calls

提问人:Barnett 提问时间:6/11/2017 最后编辑:Barnett 更新时间:6/11/2017 访问量:1013

问:

我知道 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情况。该对象仅被构造和销毁一次。 是嵌套的 NRVO 情况。同样,对象只构造一次,从不复制或移动。目前为止,一切都好。WTestTM

现在进入我的问题 - 我怎样才能以与 ?正如你在第二个对象中看到的,移动构造 - 这是我想避免的。如何更改功能以避免任何额外的副本或移动?如果你仔细想想,和没有太大区别,所以我有一种感觉,这是一定有可能的事情。TestT1TestTMTestT1transform1()TestT1TestTM

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

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

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

评论


答:

2赞 Curious 6/11/2017 #1

发生这种情况的原因是,明确不允许编译器从函数的参数列表中按值参数应用 NRVO。并且,您正在接受一个按值作为函数参数的实例,因此编译器无法省略返回时的移动。Test1Test1W

请参阅为什么 NRVO 中排除了按值参数? 以及我与霍华德·辛南特(Howard Hinnant)关于这个问题的讨论 为什么for_each通过在注释中移动返回函数

因此,您无法像在前面的情况下那样高效地工作。Test1


标准中的相关报价

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

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

    • 在具有类返回类型的函数中的语句中,当表达式是与函数返回类型相同(忽略 cv-qualification)的非易失性自动对象的名称(函数参数或由处理程序异常声明 (18.3) 引入的变量除外)时,可以通过将自动对象直接构造到函数调用的返回对象中来省略复制/移动操作return

评论

0赞 Barnett 6/11/2017
谢谢,我想我明白了。但为什么不起作用呢?我以为引用会延长临时对象的生存期?TestT2
0赞 Curious 6/11/2017
@Barnett 仅当要绑定的对象是完整对象(对于大多数目的,如果它是 prvalue)或完整对象的完整子对象(例如)时,绑定到引用的对象的生存期才会延长。而没有延长其生存期的临时词在它们出现的整个表达式中持续存在。因此,您的参数和返回值不会将绑定的任何临时对象的生存期延长到函数调用所在的表达式的生存期之外auto&& val = Something{}.member_variable&&TestT2
0赞 Curious 6/11/2017
另请参阅此处的答案 stackoverflow.com/questions/42441791/...,它们可能有助于解释更多事情
0赞 haelix 1/8/2019
@Curious对象可以以某种方式通过方法传递,例如保证保留 RVO(即避免 prvalue 具体化)?