6

Consider the following example:

#include <iostream>

struct A
{
    int n = 0;
    A() { std::cout << "A()" << std::endl; }
    A(const A&) { std::cout << "A(const A&)" << std::endl; }
    ~A() { std::cout << "~A()" << std::endl; }
};

int f(A a) { std::cout << "f()" << std::endl; return 1; }

int main()
{
    A a;
    std::cout << f(a) << " was returned from f(A)" << std::endl;
    std::cout << "leaving main..." << std::endl;
}

I expect the output of this program to be:

A()
A(const A&)
f()
~A()
1 was returned from f(A)
leaving main...
~A()

because when the function returns, its parameters should have already been destroyed. For example, if we have the following function:

A* g(A a) { std::cout << "g()" << std::endl; return &a; }

Dereferencing the returned pointer is undefined behavior:

std::cout << g(a)->n << std::endl; // undefined behavior

But the output is:

A()
A(const A&)
f()
1 was returned from f(A)
~A()
leaving main...
~A()

I'm looking for an explanation of this behavior.

Lassie
  • 853
  • 8
  • 18

1 Answers1

7

Why is destructor for a function parameter not called at the end of the function but at the end of the full expression containing the function call?

According to [expr.call]/7

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. The initialization and destruction of each parameter occurs within the context of the calling function.

So the standard allows either behaviour.

I'm looking for an explanation of this behaviour.

It's permitted by the standard, and that's how whichever compiler you tested chose to organize its code.

Useless
  • 64,155
  • 6
  • 88
  • 132
  • 1
    One of 1,545 Useless' answers... :-D – Thomas Weller Sep 13 '22 at 14:32
  • But this `std::cout << g(a)->n << std::endl;` is still undefined behavior even if the compiler chose to destroy the parameters at the end of the enclosing full-expression and not implementation-defined. Am I right? – Lassie Sep 13 '22 at 14:34
  • @Lassie: IMHO yes, because that `n` would be on the stack and the stack frame has been removed. – Thomas Weller Sep 13 '22 at 14:39
  • @ThomasWeller But accessing it in the destructor, which will be called later, is well-defined, how does that work? :D – Lassie Sep 13 '22 at 14:41
  • The full expression should be `operator<<(operator<<(cout, g(a)->n), endl)`, right? So if the parameter lifetime really ends after the full expression, it's still valid when dereferenced. But it's not _required_ still to be valid then, so it's not a great idea. – Useless Sep 13 '22 at 14:43
  • The stack frame semantics (apart from being an implementation detail even if everything doesn't get inlined) are not that straightforward. The callee's stack frame might be gone, but parameters were populated by the caller, so they must predate that frame. They're more like limited scope locals in the caller, and hence up to the caller to clean them up. – Useless Sep 13 '22 at 14:47
  • @Useless I think it's explicitly stated somewhare that this is undefined behaviour, because I just tested it and g++ 12.1 compiles `A* g(A a) { return &a; }` as `g(A):` `xor eax, eax` `ret` (so g always returns nullptr) And even if the implementation destroys the parameter at the end of the enclosing full-expression, it still cannot be used in the caller side. – Lassie Sep 13 '22 at 15:10