6

在阅读了 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 个临时对象:一个Rational(lhs)在 的主体内执行operator+,另一个在operator+通过复制第一个临时对象创建返回的值时。

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

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

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

4

4 回答 4

3

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

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

另一方面,一些习语,如表达式模板,可以通过消除临时性直接获得最终结果。

于 2018-01-24T14:23:28.267 回答
1

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

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

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

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

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

  • 绑定对纯右值的引用时

因此,在每次operator +调用执行期间都会创建一个临时的。

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

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

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

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

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

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

这产生了第 6 个临时对象。

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

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

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

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

于 2018-01-24T22:58:29.333 回答
1

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

采取形式的方法更为谨慎:

a + (b + (c + d))

在具有更高优先级的操作员可能需要它之前,它不会使用变量。但评估它需要临时工。

于 2018-01-24T14:52:11.427 回答
1

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

阅读有关as-if 规则的信息。编译器经常优化.

请参阅 CppCon 2017 Matt Godbolt “我的编译器最近为我做了什么?解开编译器的盖子”谈话

于 2018-01-24T14:56:39.427 回答