Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

What happens when mandatory RVO is applied to a reference that's extending the lifetime of a temporary?

When a reference is initialized with another reference that's extending the lifetime of a temporary, this new reference doesn't extend anything.

But what happens when mandatory RVO prevents the reference from being copied?

Consider this example: run on gcc.godbolt.org

#include <iostream>

struct A
{
    A() {std::cout << "A()\n";}
    A(const A &) = delete;
    A &operator=(const A &) = delete;
    ~A() {std::cout << "~A()\n";}
};

struct B
{
    const A &a;
};

struct C
{
    B b;
};

int main()
{
    [[maybe_unused]] C c{ B{ A{} } };
    std::cout << "---\n";
}

Under GCC this prints

A()
---
~A()

but under Clang the result is

A()
~A()
---

Which compiler is correct?

On the first glance, GCC did the right thing. But in this example:

C foo()
{
    return { B{ A{} } };
}

int main()
{
    [[maybe_unused]] C c = foo();
    std::cout << "---\n";
}

the lifetime of A surely can't be extended beyond the function (and both compilers agree on this).

Since this snippet supposedly has the same RVO as the first one, shouldn't the behavior be the same? Thus Clang's behavior seems more consistent.

like image 924
HolyBlackCat Avatar asked Jul 11 '21 10:07

HolyBlackCat


1 Answers

GCC is right.

In the second example we don’t have lifetime extension because of [class.temporary] ¶6.11:

The lifetime of a temporary bound to the returned value in a function return statement ([stmt.return]) is not extended; the temporary is destroyed at the end of the full-expression in the return statement.

If we re-wrote the example thus:

C foo(const A &a)
{
    return { B{ a } };
}

int main()
{
    C c = foo(A {});
    std::cout << "---" << std::endl;
}

clause 6.9 would instead kick in:

A temporary object bound to a reference parameter in a function call ([expr.call]) persists until the completion of the full-expression containing the call.

Why then does lifetime extension apply in the first example? Well, it’s simple: aggregate initialisers are not function calls. They are described in different portions of the standard: function calls are described in [expr.call], while initialisation expressions are described in [expr.type.conv] (and aggregate initialisation in [dcl.init.aggr]).

Note, however, that if B had an actual constructor:

struct B
{
    const A &a;
    B(const A &a_): a(a_) {}
};

then invoking that constructor counts as a function invocation, at which point [class.temporary] ¶6.9 becomes relevant again.0 Without it, the reference members of aggregates are treated as if they were declared directly as variables, as far as lifetimes are concerned.

If you want to perform aggregate initialisation without temporary lifetime extension like Clang (incorrectly) does, you can use parentheses instead of braces for initialisation, which will trigger [class.temporary] ¶6.10:

A temporary object bound to a reference element of an aggregate of class type initialized from a parenthesized expression-list ([dcl.init]) persists until the completion of the full-expression containing the expression-list.

Unfortunately, Clang apparently does not implement this currently, as this is a new addition to C++20 (proposal P0960). Note that the text of that proposal even explicitly spells out that GCC’s behaviour wrt the first example is what’s intended by the standard.


0 Presumably. The clause only mentions function calls as described in [expr.call], and I am having a hard time finding any clear statement in the standard that constructor calls are supposed to work the same way.

like image 52
user3840170 Avatar answered Oct 13 '22 23:10

user3840170