【问题标题】:Do C++ compilers avoid copying when returning by value?C++ 编译器在按值返回时是否避免复制?
【发布时间】:2011-11-22 23:40:10
【问题描述】:

考虑以下代码:

LargeObject getLargeObject()
{
    LargeObject glo;
    // do some initialization stuff with glo
    return glo;
}

void test()
{
    LargeObject tlo = getLargeObject();
    // do sth. with tlo;
}

一个简单的编译器会在 getLargeObject() 堆栈上创建一个本地 LargeObject glo,然后在返回时将其分配给 test() 中的 tlo,这涉及到复制操作。

但是一个聪明的编译器不应该意识到 glo 将被分配给 tlo 并因此首先使用 tlo 的内存来避免复制操作吗?导致一些东西(功能上)像:

void getLargeObject(LargeObject &lo)
{
    // do init stuff
}

void test()
{
    LargeObject lo;
    getLargeObject(lo);
}

我的猜测是,编译器会做类似的事情。但总能做到吗?有没有不能这样优化的情况?如何知道我的返回值是否被复制?

【问题讨论】:

标签: c++ copy return-value compiler-optimization


【解决方案1】:

你的猜测是正确的。是的,有些情况是无法做到的,例如:

LargeObject getLargeObject()
{
    LargeObject glo1, glo2;
    // do some initialization stuff         
    if (rand() % 2)
        return glo1;
    return glo2;
}

因为编译器不知道它会使用 glo1 还是 glo2 作为返回值。

“我如何知道我的返回值是否被复制?”

我能想到的两种方法。您可以创建嘈杂的复制构造函数。也就是说,复制构造函数具有一些可检测的副作用,例如打印消息。然后当然是老样子了。

【讨论】:

  • 在某些情况下,它甚至可以在这里这样做。 RVO 是许多平台上 ABI 的一部分,这意味着调用者保留返回变量的空间并传递地址。然后,一个好的编译器将在可能的最晚点在该位置构造一个对象,并且在追溯执行路径时,在某些情况下(例如,为此目的不需要考虑 ctor 的副作用),它可以首先检查if,然后就地构造对象。诚然,这些情况很少见,但如今的编译器即使在这里也在改进。
  • 更一般的,如果函数中有多个return,一些编译器会做RVO失败。当然,如果你在函数中同时使用glo1glo2,在某种程度上需要两个对象,有时返回一个,有时返回另一个,编译器无法避免复制。
  • 重要的是要注意,James Kanze 在他的回答中提到了这一点,但这里缺少的是,实际上有 两个 副本,一个从 gloX 到将临时和一个单独的从临时返回到test 中构造的对象。第二个副本是 PlasmaHH 提到的在 ABI 中处理的内容,并且总是执行(在我知道的所有平台上),但第一个副本取决于编译器能够知道 glo1 中的哪一个或者构造对象时会返回glo2(更多here
【解决方案2】:

是的,应该。这称为命名返回值优化(NRVO 或简称 RVO)。

【讨论】:

  • 是的,在这种特殊情况下,它是 NRVO,因为返回了一个命名变量。 RVO 是在 return 语句中创建和返回实例的时候。
【解决方案3】:

对于初学者来说,即使是天真的编译器也不会“分配给 tlo”,因为标准不允许这样做。形式语义 您的代码涉及两个副本(均使用复制构造函数);这 第一个从 glo 到一个临时返回值,第二个从这个 临时返回值到tlo。然而,该标准正式给出 编译器删除这两个副本的权利,在这个特定的 情况,实际上,我想所有的编译器都会这样做。

只要您返回一个局部变量或 临时的;如果有多个编译器,一些编译器不会这样做 return 在代码中,但是(但情况永远不会如此) 编写代码)。

第二个副本的压制取决于你是 在调用站点构造一个新对象。如果你不建造 一个新对象,那么甚至可能没有第二个副本要压制;例如 在像getLargeObject().memberFunction() 这样的情况下。如果你要分配 但是,对于现有对象,编译器无能为力;它 必须调用赋值运算符。如果赋值运算符复制, 然后你得到那个副本。

【讨论】:

  • 哇,这个答案中有一些有趣的信息!我不确定我是否可以解决问题。第一句话和最后一句话在我看来是矛盾的。还是只是语言问题?
  • @James:但在编写良好的代码中永远不会出现这种情况 - 哇 - SO 没有政治! :)
  • @Ben 问题在于,在第一句话中,我在调用复制构造函数的意义上使用了 copy ---我应该更准确。在最后一句中,我在更一般的意义上使用它:赋值运算符可能会“复制”大量数据。 (事实上​​,一个大对象的典型赋值运算符可能会复制构造一个新实例,然后交换数据。)
  • @James:仍然不清楚 :( 你在第一句话中没有使用“复制”。标准中不允许什么?RVO 是否只摆脱第一次复制操作?如果我通过了对被调用函数的引用根本就没有复制操作……不能用类似的方式优化吗?
  • @Ben RVO 基本上只删除了第一个副本。第二个副本取决于您在呼叫站点所做的事情。如果您正在构建一个新对象,编译器可以安排返回的临时对象在其位置。如果你正在做其他事情,它通常不能。标准不允许的是在作业中不调用operator=;如果你在operator= 里面做一个副本(经常出现的情况),那么这个副本就无法优化出来。
猜你喜欢
  • 2012-11-22
  • 1970-01-01
  • 2015-09-19
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2012-09-15
  • 2011-08-14
相关资源
最近更新 更多