理解复制构造函数调用和命名返回值优化

3

我一直在研究有关NRVO的这篇文章


(NRVO是C++中一种优化技术,称为返回值优化)
  class RVO
  {
    public:
    RVO(){
          printf("I am in constructor\n"); }
    RVO(const RVO& c_RVO) { 
          printf("I am in copy constructor\n"); }
    ~RVO(){
          printf("I am in destructor\n"); }
    int mem_var;
  };
  RVO MyMethod(int i)
  {
     RVO rvo;
     rvo.mem_var = i;
     return (rvo);
  }
  int main()
  {
        RVO rvo;
        rvo=MyMethod(5);
  }

以下是 Visual Studio 上的输出,这是我对其理解的方式。
 I am in constructor       // main rvo construction
 I am in constructor       //MyMethod rvo construction 
 I am in copy constructor  //temporary created inside MyMethod
 I am in destructor        //Destroying rvo in MyMethod
 I am in destructor        //Destroying temporary in MyMethod
 I am in destructor        //Destroying rvo of main

如果我将主要部分写成以下方式:
 int main()
 { 
    RVO rvo = MyMethod(5);
    return 0;
 }

输出如下,如何理解它?
 I am in constructor       //MyMethod rvo construction 
 I am in copy constructor  //temporary created inside MyMethod
 I am in destructor        //Destroying rvo in MyMethod
 I am in destructor        //Destroying rvo of main

为什么第二个版本中的Mymethod中的临时变量没有被销毁?

为什么在RVO rvo = MyMethod(5);中不调用复制构造函数?我认为在第二个版本中应该调用两次复制构造函数,一次是在Mymethod内部创建的临时变量,另一次是在RVO rvo = MyMethod(5);中。 我知道某些调用可能被省略了。 能否有人帮忙解释这些调用。

编辑: 使用return rvo而不是return (rvo)会改变输出结果

第一种情况

 I am in constructor
 I am in constructor
 I am in destructor
 I am in destructor

第二个案例

 I am in constructor
 I am in destructor       

我猜当我去掉括号时,NRVO就开始起作用了。但我更感兴趣的是第一个输出,当没有优化时。


@GauravSehgal:没错,g++不进行优化。但是Visual C++会应用RVO,这是应该的。 - Cheers and hth. - Alf
去掉返回值中的括号 http://ideone.com/vfHN5W - Chris Drew
@ChrisDrew:谢谢!不过这没什么意义。我想知道是否已经报告为错误了? - Cheers and hth. - Alf
@BenjaminLindley:谢谢你也!抱歉我没有第一时间看到它。在我看来,这似乎是编译器的一个错误;括号并不会使表达式成为右值。 - Cheers and hth. - Alf
@Cheersandhth.-Alf:我不确定这是否是一个错误。有关NRVO的相关标准文本(12.8/31)说:“当表达式是非易失性自动对象的名称时”--如果您在名称周围加上括号,它不仅仅是名称。 - Benjamin Lindley
显示剩余4条评论
2个回答

0

第一个析构函数调用来自于主函数中的rvo对象的销毁。必须删除首先创建的对象。这不是复制赋值,而是复制构造。


0

更新:针对更新后程序的输出,使用return rvo而不是return (rvo);

I am in constructor
I am in constructor
I am in destructor
I am in destructor

你看到这个的原因是因为两个对象(MyMethod::rvomain::rvo)都经历了默认构造,然后后者被分配为单独的操作,但你没有记录它。

通过输出对象的地址和调用函数时的this指针值,您可以更好地了解正在发生的情况:

#include <cstdio>
#include <iostream>
class RVO
  {
    public:
    RVO(){
          printf("%p constructor\n", this); }
    RVO(const RVO& c_RVO) {
          printf("%p copy constructor, rhs %p\n", this, &c_RVO); }
    ~RVO(){
          printf("%p destructor\n", this); }
    int mem_var;
  };
  RVO MyMethod(int i)
  {
     RVO rvo;
     std::cout << "MyMethod::rvo @ " << &rvo << '\n';
     rvo.mem_var = i;
     return (rvo);
  }
  int main()
  {
        RVO rvo=MyMethod(5);
        std::cout << "main::rvo @ " << &rvo << '\n';
  }

输出结果还取决于您是否使用优化进行编译;您链接到了微软文档,所以也许您正在使用微软编译器 - 尝试使用 cl /O2

为什么在第二个版本中 Mymethod 中的临时对象没有被销毁?

那里没有临时对象 - 在 main 中的对象是直接进行复制构造的。让我们逐步分析一下:

002AFA4C constructor
MyMethod::rvo @ 002AFA4C   // MyMethod::rvo's constructed

002AFA70 copy constructor, rhs 002AFA4C   // above is copied to 2AFA70
002AFA4C destructor        // MyMethod::rvo's destructed
main::rvo @ 002AFA70       // turns out the copy above was directly to main::rvo
002AFA70 destructor        // main::rvo's destruction

[Alf的评论如下] "直接复制构造"对我来说并不完全有意义。我认为OP指的是rvo局部变量。
考虑程序第一个版本的增强输出(未经优化):
002FF890 constructor  // we find out this is main::rvo below
002FF864 constructor  // this one's MyMethod::rvo
MyMethod::rvo @ 002FF864
002FF888 copy constructor, rhs 002FF864  // 2FF888 is some temporary
002FF864 destructor   // there goes MyMethod::rvo
002FF888 destructor   // there goes the temporary
main::rvo @ 002FF890
002FF890 destructor   // and finally main::rvo

如果我们将其与 OP 的输出和注释联系起来...
I am in constructor       // main rvo construction
I am in constructor       //MyMethod rvo construction 
I am in copy constructor  //temporary created inside MyMethod
I am in destructor        //Destroying rvo in MyMethod
I am in destructor        //Destroying temporary in MyMethod
I am in destructor        //Destroying rvo of main

OP(正确地)将复制构造的对象称为临时对象。当我说第二个程序版本中“那里没有临时对象 - main 中的对象是直接复制构造的”时,我的意思是在我们直接分析的第一个程序中没有与此相当的临时对象,而是从 MyMethod::rvo 复制构造了 main::rvo


“直接复制构造”对我来说并不完全有意义。我认为OP的意思是rvo本地变量。 - Cheers and hth. - Alf
1
@Cheersandhth.-Alf: 我在最新的编辑中逐步进行 - 希望这很清楚。干杯。 - Tony Delroy
@TonyD 你提到的第二个案例的输出显示进行了一些优化,对吗?我关闭了优化并运行了第二个案例,但即使这样仍然得到了相同的输出(只是进行了一些优化)。如果优化关闭,则应有两个复制构造函数调用,对吗? - Gaurav Sehgal
@GauravSehgal:大多数编译器都会尝试在优化级别上不改变正确程序的可观察行为。因此,所有适用的复制省略形式都会执行,而不管优化级别如何。但是,有些编译器有一个选项可以明确地关闭它。我相信对于gcc(和可能也是clang),它是-fno-elide-constructors - Benjamin Lindley
无论MyMethod如何实现,这个调用不应该像RVO rvo = rvo2那样调用复制构造函数吗?而另一个则会在创建临时对象时出现。我考虑没有优化。 - Gaurav Sehgal
显示剩余7条评论

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