在没有 return 值优化的情况下将两个对象加在一起时会创建多少个临时对象?

How many temporary objects are created when two objects are added together without the return value optimization?

在阅读 Scott Meyers 的书 "More Effective C++" 的第 20 和 22 条后,我决定问这个问题。

假设您写了一个 class 来表示有理数:

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;
}

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

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

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

当 Scott 介绍这个操作时,我感到困惑:

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

并写道:"Probably uses 3 temporary objects, one for each call to operator+"。我相信如果 return 值优化 被禁用,上面的操作将使用 6 个临时对象(每次调用 operator+ 2 个),而如果它是启用后,上面的操作将完全不使用临时对象。斯科特是如何得出他的结果的?我认为这样做的唯一方法是部分应用 return 值优化.

我觉得你考虑的太多了,尤其是优化的细节。

对于result = a + b + c + d;,作者只是想声明将创建3个临时文件,第一个是针对a + b的结果,然后第二个是针对[=12的结果=],第三个是给temporary#2 + d赋值给result的。之后,3 个临时物被销毁。所有临时值仅用作中间结果。

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

编译器可能会检测累加并应用优化,但通常从左到右移动和减少表达式在某种程度上是棘手的,因为它可能会被样式 a + b * c * d

的表达式击中

形式比较谨慎:

a + (b + (c + d))

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

编译器没有创建任何变量。因为变量是那些出现在源代码中的变量,并且 变量在执行时 或可执行文件中不存在(它们可能成为内存位置,或者 "ignored")。

了解 as-if rule. Compilers are often optimizing

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

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

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

even if the implicit object parameter is not const-qualified, an rvalue can be bound to the parameter as long as in all other respects the argument can be converted to the type of the implicit object parameter.

然后要将纯右值绑定到引用,必须发生临时物化 [class.temporary]/2.1

Temporary objects are materialized [...]:

  • when binding a reference to a prvalue

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

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

  1. 一个用于实现 a+b
  2. 的结果
  3. 另一个实现(a+b)+c
  4. 的结果

所以此时已经创建了 4 个临时文件。然后,第三次调用 operator+ 在函数体内创建第 5 个临时变量

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

Temporary objects are materialized [...]:

  • when a prvalue appears as a discarded-value expression.

产生第 6 个临时变量。

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

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

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

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