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

4

6

I decided to ask this question after reading items 20 and 22 of the book "More Effective C++" by Scott Meyers.

Let's say you wrote a class to represent rational numbers:

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

Now let's say that you decided to implement operator+ using operator+=:

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

My question is: if the return value optimization were disabled, how many temporary variables would be created by operator+?

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

I believe 2 temporaries are created: one when Rational(lhs) is executed inside the body of operator+, and another when the value returned by operator+ is created by copying the first temporary.

My confusion arose when Scott presented this operation:

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

And wrote: "Probably uses 3 temporary objects, one for each call to operator+". I believe that if the return value optimization were disabled, the operation above would use 6 temporary objects (2 for each call to operator+), while if it were enabled, the operation above would use no temporaries at all. How did Scott arrive at his result? I think the only way to do so would be to partially apply the return value optimization.

Postfree answered 24/1, 2018 at 14:15 Comment(15)
You can instrument the destructor (have it print something), and this way count how many temporaries are actually created by your compiler. However, you would be hard-pressed to find a compiler where you can disable RVO; all modern compilers I know of perform it unconditionally, even in debug builds with optimizations disabled. Moreover, C++17 standard makes RVO mandatory, rendering the question unequivocally moot.Monolatry
do not return const Rational - I mean skip const (but not related to question)Michaeu
This is a nice question (+1) but I dislike the sentiment intensely. You are asking a "what happens if a soon-to-be-compulsory" feature of C++ is disabled?Annalisaannalise
How many temporary variables would be created by operator+ -- Doesn't this really depend on the compiler, even if you were able to turn off RVO? A "bad" compiler may create more temporaries than a "good" compiler, but still achieve the same end result.Donadonadee
@PaulMcKenzie: I think there is an upper limit especially if the constructor or destructor has side-effects.Annalisaannalise
@Annalisaannalise -- If the quote from the OP is correct, it looks like even Scott M. isn't sure -- Probably uses 3 temporary objects, -- the keyword being "Probably".Donadonadee
@PaulMcKenzie: I'm hoping that the great Scott M. is using "Probably" to mean "Upper bounded by".Annalisaannalise
@Donadonadee The quote is taken directly from page 108 of the book.Postfree
@Annalisaannalise wouldn't the upper bound be 6 temporaries?Postfree
@user3266738: Strictly no more than 3, by my reading. The expression is grouped as ((a + b) + c) + dAnnalisaannalise
@Annalisaannalise But what about the temporaries created inside operator+ when Rational(lhs) is executed to be able to call operator+=? If you consider those, the upper bound is 6? Or are those optimized out of existence in some way?Postfree
@user3266738: FYI: "if the return value optimization were disabled, how many temporary variables would be created by operator+?" The same number as would be created if RVO were enabled.Ichthyology
Assuming no RVO: (a+b) creates a temporary and creates another to be passed to (b + c), (b+c) creates a temporary and another to be passed to (c+d), (c+d) creates a temporary and another which is assigned to result. That leads to 6 temporaries. The key thing to note is that ((a+b) + c) will pass the result of (a+b) directly to operator+ even without RVO since it's passed by const reference, not by value. Meanwhile with RVO, all those "return and pass somewhere else" temps are eliminated, and you end up with half the temporaries.Urethroscope
Not saying that's how things actually work but probably how he arrived at the estimate of "probably 6 temporaries" without RVO. There's no "partial RVO" required to pass the result of operator+ to another invocation of operator+ provided the operands are accepted by const ref. There's a temp involved local to the function, another to return without RVO, but not another to pass the result as an operand to another call to operator+.Urethroscope
So that leads to 3 invocations of operator+, and each one creates two temporaries (one local, another to return the result by value without RVO).Urethroscope
E
3

I think you're just considering too much, especially with the details of optimization.

For result = a + b + c + d;, the author just want to state that 3 temporaries will be created, the 1st one is for the result of a + b, then 2nd one is for the result of temporary#1 + c, the 3rd one is for temporary#2 + d and then it's assigned to result. After that, 3 temporaries are destroyed. All the temporaries are only used as the intermediate results.

On the other hand, some idioms such like expression templates could make it possible to get the final result directly with elimination of temporaries.

Eduction answered 24/1, 2018 at 14:23 Comment(2)
correct me if I am wrong, but even with Expression templates you still have all the temporaries. That is the language syntax. With expression templates you just delay the computation to the very end (before the assignment).Ruy
@Ruy (If my memory is accurate) suppose the operator+ is implemented as return Ratinal(lhs.numerator + rhs.numerator, lhs.denominator + rhs.denominator), then expression templates could expand a + b + c + d to Ratinal(a.numerator + b.numerator + c.numerator + d.numerator, a.denominator + b.denominator + c.denominator + d.denominator, at least we don't need 3 temporaries.Eduction
A
1

Compiler may detect accumulation and apply optimizations but generally shifting and reducing an expression from left to right is somehow tricky as it may be hit by an expression of style a + b * c * d

It is more cautious to take approach of form:

a + (b + (c + d))

which will not consume a variable before it might be required by an operator with higher priority. But evaluating it requires temporaries.

Anemo answered 24/1, 2018 at 14:52 Comment(0)
C
1

No variables are created by the compiler. Because variables are those appearing in the source code, and variables don't exist at execution time, or in the executable (they might become memory locations, or be "ignored").

Read about the as-if rule. Compilers are often optimizing.

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

Containerize answered 24/1, 2018 at 14:56 Comment(0)
M
1

In the expression a+b+c+d 6 temporaries will be created and destroyed, this is mandatory (with and without RVO). You can check it here.

Inside operator + definition, in the expression Rational(lhs)+=a, the prvalue Rational(lhs) will be bound to the implied object parameter of operator+= which is authorized according to this very specific rule [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.

Then to bind a prvalue to a reference, temporary materialization must occurs [class.temporary]/2.1

Temporary objects are materialized [...]:

  • when binding a reference to a prvalue

So a temporary is created during the excution of each operator + call.

Then the expression Rational(lhs)+=a which once is returned can conceptualy seen as Rational(Rational(lhs)+=a) is a prvalue (a prvalue is an expression whose evaluation initializes an object - phi:an object in power) which is then bound to the first argument of the 2 subsequent calls to operator +. The cited rule [class.temporary]/2.1 applies twice again and will create 2 temporaries:

  1. One for materializing the result of a+b,
  2. the other for materializing the result of (a+b)+c

So at this point 4 temporaries have been created. Then, the third call to operator+ creates the 5th temporary inside the function body

Finaly the result of the last call to operator + is a discarded value expression. This last rule of the standard applies [class.temporary]/2.6:

Temporary objects are materialized [...]:

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

Which produces the 6th temporaries.

Without RVO the return value is directly materialized, which makes temporary materialization of the return prvalues not necessary anymore. This is why GCC produces the exact same assembly with and without -fno-elide-constructors compiler option.

In order to avoid temporary materialization, you could define operator +:

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

With such a definition, the prvalue a+b and (a+b)+c would be directly used to initialize to first parameter of operator + which would save you from the materialization of 2 temporaries. See the assembly here.

Michaelamichaele answered 24/1, 2018 at 22:58 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.