【问题标题】:How many temporary objects are created when two objects are added together without the return value optimization?当两个对象相加而没有返回值优化时,会创建多少个临时对象?
【发布时间】:2023-03-12 15:45:01
【问题描述】:

在阅读了 Scott Meyers 的《More Effective C++》一书的第 20 和 22 条后,我决定提出这个问题。

假设你写了一个类来表示有理数:

class Rational
{
public:
    Rational(int numerator = 0, int denominator = 1);

    int numerator() const;
    int denominator() const;

    Rational& operator+=(const Rational& rhs); // Does not create any temporary objects
    ...
};

现在假设您决定使用operator+= 实现operator+

const Rational operator+(const Rational& lhs, const Rational& rhs)
{
    return Rational(lhs) += rhs;
}

我的问题是:如果返回值优化被禁用,operator+会创建多少临时变量?

Rational result, a, b;
...
result = a + b;

我相信创建了 2 个临时对象:一个是在 operator+ 的主体内执行 Rational(lhs) 时,另一个是通过复制第一个临时对象创建 operator+ 返回的值。

当 Scott 介绍这个操作时,我产生了困惑:

Rational result, a, b, c, d;
...
result = a + b + c + d;

并写道:“可能使用 3 个临时对象,每次调用 operator+ 一个”。我相信如果 返回值优化 被禁用,上面的操作将使用 6 个临时对象(每个调用 operator+ 2 个),而如果它被启用,上面的操作将不使用临时对象一点也不。斯科特是如何得出他的结果的?我认为这样做的唯一方法是部分应用返回值优化

【问题讨论】:

  • 您可以检测析构函数(让它打印一些东西),这样可以计算编译器实际创建了多少临时对象。但是,您很难找到可以禁用 RVO 的编译器。我所知道的所有现代编译器都无条件执行它,即使在禁用优化的调试版本中也是如此。此外,C++17 标准规定 RVO 是强制性的,这使得这个问题毫无意义。
  • 这是一个很好的问题 (+1),但我非常不喜欢这种情绪。您是在问“如果禁用 C++ 的即将成为强制性的”功能会发生什么?
  • operator+ 会创建多少个临时变量 -- 这是否真的取决于编译器,即使您能够关闭 RVO?一个“坏”的编译器可能比一个“好的”编译器创建更多的临时文件,但仍能达到相同的最终结果。
  • @Bathsheba -- 如果 OP 的引用是正确的,看起来甚至 Scott M. 都不确定 -- 可能使用 3 个临时对象, --关键字是“可能”。
  • @user3266738:仅供参考:“如果禁用了返回值优化,operator+ 会创建多少临时变量?”如果 RVO 被禁用,则创建的数量相同已启用。

标签: c++ operator-keyword copy-elision return-value-optimization effective-c++


【解决方案1】:

我认为您只是考虑太多,尤其是优化的细节。

对于result = a + b + c + d;,作者只想说明会创建3个临时对象,第一个是a + b的结果,第二个是temporary#1 + c的结果,第三个是temporary#2 + d,然后分配给result。之后,3个临时工被摧毁。所有的临时变量只作为中间结果。

另一方面,像expression templates这样的一些成语可以通过消除临时性直接得到最终结果。

【讨论】:

  • 如果我错了,请纠正我,但即使使用表达式模板,您仍然拥有所有临时模板。那就是语言语法。使用表达式模板,您只需将计算延迟到最后(分配之前)。
  • @bolov(如果我没记错的话)假设operator+被实现为return Ratinal(lhs.numerator + rhs.numerator, lhs.denominator + rhs.denominator),那么表达式模板可以将a + b + c + d扩展为Ratinal(a.numerator + b.numerator + c.numerator + d.numerator, a.denominator + b.denominator + c.denominator + d.denominator,至少我们不需要3临时工。
【解决方案2】:

编译器可能会检测累积并应用优化,但通常从左到右移动和减少表达式有点棘手,因为它可能会受到样式 a + b * c * d 的表达式的影响

采取形式的方法比较谨慎:

a + (b + (c + d))

在具有更高优先级的运算符可能需要变量之前,它不会使用变量。但评估它需要临时性。

【讨论】:

    【解决方案3】:

    编译器不会创建任何变量。因为变量是出现在源代码中的变量,而变量在执行时不存在,或者在可执行文件中(它们可能成为内存位置,或者被“忽略”)。

    阅读as-if rule。编译器通常是optimizing

    参见 CppCon 2017 Matt Godbolt “What Has My Compiler Done for Me Lately? Unbolting the Compiler's Lid” talk

    【讨论】:

      【解决方案4】:

      在表达式a+b+c+d 中,将创建和销毁 6 个临时对象,这是强制性的(有和没有 RVO)。您可以查看here

      operator + 定义中,在表达式Rational(lhs)+=a 中,纯右值Rational(lhs) 将绑定到operator+=隐含对象参数,这是根据这个非常具体的规则@ 授权的987654322@(参考[expr.call]/4

      即使隐式对象参数不是 const 限定的,也可以将右值绑定到参数,只要在所有其他方面参数可以转换为隐式对象参数的类型。

      然后要将prvalue绑定到引用,必须发生临时实现[class.temporary]/2.1

      临时对象被物化[...]:

      • 将引用绑定到纯右值时

      因此在每个operator + 调用的执行过程中都会创建一个临时的。

      那么表达式Rational(lhs)+=a 可以在概念上 看作Rational(Rational(lhs)+=a) 是一个prvalue(prvalue 是一个表达式,其评估初始化一个对象 - phi:an object in power) 然后绑定到对operator + 的 2 个后续调用的第一个参数。引用的规则 [class.temporary]/2.1 再次应用两次,将创建 2 个临时对象:

      1. 用于具体化a+b 的结果,
      2. 另一个用于实现(a+b)+c的结果

      所以此时已经创建了 4 个临时对象。然后,对operator+ 的第三次调用会在函数体内创建第 5 个临时对象

      最后一次调用operator + 的结果是一个丢弃的值表达式。该标准的最后一条规则适用于 [class.temporary]/2.6:

      临时对象被物化[...]:

      • 当纯右值显示为废弃值表达式时。

      产生第 6 个临时对象。

      没有 RVO,返回值是直接实现的,这使得返回纯右值的临时实现不再是必要的。这就是为什么 GCC 在有和没有 -fno-elide-constructors 编译器选项的情况下生成完全相同的程序集的原因。

      为了避免临时实现,你可以定义operator +:

      const Rational operator+(Rational lhs, const Rational& rhs)
      {
          return lhs += rhs;
      }
      

      有了这样的定义,prvalue a+b(a+b)+c 将直接用于初始化operator + 的第一个参数,这将使您免于实现 2 个临时对象。请参阅程序集here

      【讨论】:

        猜你喜欢
        • 1970-01-01
        • 2016-01-30
        • 2012-02-26
        • 1970-01-01
        • 2011-08-17
        • 1970-01-01
        • 1970-01-01
        • 2014-11-26
        • 2023-03-31
        相关资源
        最近更新 更多