Sequencing of function parameter destruction
Asked Answered
A

1

16

According to C++14 [expr.call]/4:

The lifetime of a parameter ends when the function in which it is defined returns.

This seems to imply that a parameter's destructor must run before the code which called the function goes on to use the function's return value.

However, this code shows differently:

#include <iostream>

struct G
{
    G(int): moved(0) { std::cout << "G(int)\n"; }
    G(G&&): moved(1) { std::cout << "G(G&&)\n"; }
    ~G() { std::cout << (moved ? "~G(G&&)\n" : "~G()\n"); }

    int moved;
};

struct F
{
    F(int) { std::cout << "F(int)\n"; }
    ~F() { std::cout << "~F()\n"; }
};

int func(G gparm)
{
    std::cout << "---- In func.\n";
    return 0;
}


int main()
{
    F v { func(0) };
    std::cout << "---- End of main.\n";
    return 0;
}

The output for gcc and clang , with -fno-elide-constructors, is (with my annotations):

G(int)               // Temporary used to copy-initialize gparm
G(G&&)               // gparm
---- In func.
F(int)               // v
~G(G&&)              // gparm
~G()                 // Temporary used to copy-initialize gparm
---- End of main.
~F()                 // v

So, clearly v's constructor runs before gparm's destructor. But in MSVC, gparm is destroyed before v's constructor runs.

The same issue can be seen with copy-elision enabled, and/or with func({0}) so that the parameter is direct-initialized. v is always constructed before gparm is destructed. I also observed the issue in a longer chain, e.g. F v = f(g(h(i(j()))); did not destroy any of the parameters of f,g,h,i until after v was initialized.

This could be a problem in practice, for example if ~G unlocks a resource and F() acquires the resource, it would be a deadlock. Or, if ~G throws, then execution should jump to a catch handler without v having been initialized.

My question is: does the standard permit both of these orderings? . Is there any more specific definition of the sequencing relationship involving parameter destruction, than just that quote from expr.call/4 which does not use the standard sequencing terms?

Auria answered 3/10, 2016 at 4:57 Comment(6)
If the destruction was mandatory at the end of the function, wouldn't this prevent copy-elision in a lot of cases (if you construct the returned object using the parameter)?Pneumo
@Pneumo I don't think it's a copy-elision context, returning a parameter. (But even if it is, that's no problem, as it would get elided into the return value, so there is no destruction of the parameter any more)Auria
I meant returning an object constructed using the parameter - Let's say I have X f(A a) which return X(a), if I do X x(f(A())), compiler will likely construct x in place, but if A() was meant to be destroyed at the end of f(), how would this be done (you would need to construct X from a, then destroy a, then copy X, no?)?Pneumo
@Pneumo in X x(f(A())), A() is the argument, not the parameter. The argument is destroyed at the end of the full-expression. The parameter is the symbol a inside f, which is copy-initialized from the argument. If that copy-initialization is elided then the argument is never destroyed ; there is the construction A() and the destruction of the parameter.Auria
I was talking about the elision of x's constructor, not a's - If a has to be destroyed at the end of f, how can you construct X in-place using a without modifying the order in the observable behavior (but maybe copy-elision is allowed to do that?)? What I try to say is that without elision, the behavior should be (I think) X(const A&) -> ~A() -> X(X&&), and with the elision it becomes X(const A&) -> ~A(), so in the first case, a is destroyed before the construction (by copy) of x, while in the second case a is destroyed after - But maybe this is allowed?Pneumo
OK,I see what you are saying now. MSVC uses the X(A&) -> ~A -> X(X&&) order, gcc uses X(A&) -> X(X&&) -> ~A. So MSVC's elision does change the ordering between ~A and construction of the caller's X. But I think that is OKAuria
A
13

Actually I can answer my own question... didn't find an answer while searching before writing it, but then searching again afterwards did find an answer (typical huh).

Anyway: this issue is CWG #1880 with the following note:

Notes from the June, 2014 meeting:

WG decided to make it unspecified whether parameter objects are destroyed immediately following the call or at the end of the full-expression to which the call belongs.

although the issue 1880 remains open.

The subject was also visited by P0135 - guaranteed copy-elision which made it implementation-defined, rather than unspecified. In C++17 (N4659) the text is:

It is implementation-defined whether the lifetime of a parameter ends when the function in which it is defined returns or at the end of the enclosing full-expression.

There is more background information here: Late destruction of function parameters


Note: The definition of full-expression can be found in C++14 [intro.execution]/10:

A full-expression is an expression that is not a subexpression of another expression. [...] If a language construct is defined to produce an implicit call of a function, a use of the language construct is considered to be an expression for the purposes of this definition.

So F v { func(0) }; is the enclosing full-expression for gparm (even though it's a declaration and not an expression!).

Auria answered 3/10, 2016 at 5:6 Comment(5)
Uh huh, exactly what is the full-expression in the declaration F v { func(0) };?Apfelstadt
@Cheersandhth.-Alf F v { func(0) }; is the full-expression. See [intro.execution]/10Auria
Thanks,if that's a definition then I think you should quote that, because the answer's not meaningful without such definition. ;-)Apfelstadt
This reminds me, we need a way to generalize reference-capture lifetime-extension in C++.Brazilein
Maybe v { func(0) } is full-expression?Pipestone

© 2022 - 2024 — McMap. All rights reserved.