15

I was pretty sure, that destructors for function's parameters should be called at the exit of corresponding function. Consider 5.2.2p4 of C++11 Standard:

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

However, let's try this code:

#include <iostream>
using namespace std;

struct Logger {
    Logger(int) { cout << "Construct " << this << '\n'; }
    Logger(const Logger&) { cout << "Copy construct " << this << '\n'; }
    ~Logger() { cout << "Destruct " << this << '\n'; }
};

int f(Logger)
{
    cout << "Inside f\n";
    return 0;
}

int main()
{
    f(f(f(10)));
}

After compilation with gcc or clang, the output will be like this:

Construct 0x7fffa42d97ff
Inside f
Construct 0x7fffa42d97fe
Inside f
Construct 0x7fffa42d97fd
Inside f
Destruct 0x7fffa42d97fd
Destruct 0x7fffa42d97fe
Destruct 0x7fffa42d97ff

As we can see, all three parameters were destroyed only after the last function call was finished. Is this correct behaviour?

  • It's a recursive call, so I think all three instances are destroyed after the outermost call of "f" is done. – PiotrK Nov 28 '14 at 21:11
  • That's intended to guarantee consistency. See [here](http://stackoverflow.com/questions/2506793/c-life-span-of-temporary-arguments). – BaCaRoZzo Nov 28 '14 at 21:11
  • @NoelPerezGonzalez Did you call the function recursively as shown in the example? – Grice Nov 28 '14 at 21:15
  • @PiotrK It isn't a recursion (which is the call to the same function inside that function body). Also you can get the same behaviour with this code: `f(10), f(10), f(10);` – Bevel Lemelisk Nov 28 '14 at 22:25
  • 2
    A summary of the other question: you're entirely correct in your reading the standard, but the standard does not correctly reflect the intent, and the intent is to allow your compiler's behaviour as well. –  Nov 29 '14 at 00:52
  • Calling a function f(f(f(10))) is "calling it recursively", which is not the same as saying something is a "recursive function". I am seeing @NoelPerezGonzalez's output in VS2013, but I'm seeing the OP's output in gcc. Sounds like an implementation detail. – Addy Nov 29 '14 at 14:26
  • The linked dulicate question was on hold for being unclear so that is not a fair comparison. – Martin York Nov 29 '14 at 17:38
  • 1
    _"As we can see, all three parameters were destroyed only after the last function call was finished."_ We can see no such thing. I'm sure you're misinterpreting this output. – Lightness Races in Orbit Nov 29 '14 at 18:03
  • 1
    @LokiAstari [That other question](http://stackoverflow.com/questions/27157204/whats-the-life-time-of-a-function-parameter-citation-needed) shouldn't have been closed in the first place, but even if it was, how does that make a difference? Regardless, it does ask the same thing, and the answers there do provide an answer to this question. –  Nov 29 '14 at 18:38
  • @hvd: It doesn't "contradict" anything. The parameter's lifetime ends, sure, but the temporary that you constructed in `main` to copy into that parameter is a different object (notwithstanding copy elision, which is obviously in play here). Those temporaries of course live until the end of that line in `main`; yes, all of them. – Lightness Races in Orbit Nov 29 '14 at 18:45
  • 1
    @hvd: No, copy elision is allowed to remove such side effects. And yes there are absolutely temporaries constructed in `main`. The literal `10` (and the return value of each call) is used to construct a value to copy into the parameter for `f`. If only the OP had quoted the _whole_ paragraph. – Lightness Races in Orbit Nov 29 '14 at 18:49
  • 1
    @LightnessRacesinOrbit I see your point now. You're right. There is a compiler bug (at least as far as the standard is concerned), but this question doesn't demonstrate the compiler bug, and I agree that the output the OP is getting is a valid output that could legitimately be printed by a conforming implementation. Compile the exact same program with `g++ -fno-elide-constructors` though, and the output does indicate the compiler bug. (Which I now see dyp has also pointed out on one answer, with `clang++ -fno-elide-constructors`.) –  Nov 29 '14 at 18:59
  • @hvd I do not know if this really is a bug; both g++ and clang++ behave in the same way for this program when using `-fno-elide-constructors`. – dyp Nov 29 '14 at 19:08
  • @dyp: That doesn't mean it's not a bug. – Lightness Races in Orbit Nov 29 '14 at 19:08
  • 3
    @dyp It is an area in which compilers do not conform to C++11, [but the standard is being changed to allow the compilers' behaviour](http://www.open-std.org/jtc1/sc22/wg21/docs/cwg_active.html#1880). –  Nov 29 '14 at 19:08
  • @LightnessRacesinOrbit Indeed, but it makes me suspicious. Maybe it has something to do with that compiler switch? Or maybe we're understanding the Standard in a different way than the implementers? – dyp Nov 29 '14 at 19:09
  • @hvd: I think you have the basis for a really excellent and authoritative answer, now! – Lightness Races in Orbit Nov 29 '14 at 19:09
  • @hvd I agree with Lightness. We have all the puzzle pieces now. – dyp Nov 29 '14 at 19:10
  • @LightnessRacesinOrbit It's what I had answered on [this question](http://stackoverflow.com/questions/27157204/whats-the-life-time-of-a-function-parameter-citation-needed), which I had closed this question as a duplicate of. I'm not posting it as a new answer here, that just gets me accusations of rep ****ing. –  Nov 29 '14 at 19:11
  • @hvd: Well they're obviously not duplicate _questions_. I'd just copy the answer, frankly. Perhaps cite yourself. I don't consider that to be "rep-****ing" in the slightest: in both cases you deserve rep for accurately answering the question. – Lightness Races in Orbit Nov 29 '14 at 19:12
  • 1
    @hvd I think an answer to *this* question needs to mention copy elision (and how this makes the compiler's behaviour compliant in the OP's case). – dyp Nov 29 '14 at 19:12
  • @dyp That's a good point. Will see if I can write something new yet decent up. –  Nov 29 '14 at 19:13
  • Oh yeah, plus what dyp said. – Lightness Races in Orbit Nov 29 '14 at 19:13

4 Answers4

8

See the C++11 Standard, §12.2/3, saying

Temporary objects are destroyed as the last step in evaluating the full-expression (1.9) that (lexically) contains the point where they were created.

David Rodríguez - dribeas
  • 204,818
  • 23
  • 294
  • 489
marc
  • 6,103
  • 1
  • 28
  • 33
  • 3
    I don't think function's parameters are counted as temporary objects. At least they're not listed as a context in 12.2p1. – Bevel Lemelisk Nov 28 '14 at 22:19
  • Imho 12.2p2 makes it quite clear that function parameters as well as function return values may be temporary. – marc Nov 28 '14 at 22:24
  • I don't agree, read the text below this example: "An implementation might use a temporary in which to construct X(2) before passing it to f() using X’s copy constructor; alternatively, X(2) might be constructed in the space used to hold the argument." This example is about how temporary creation can be used or avoided for function arguments passing (copy elision). – Bevel Lemelisk Nov 28 '14 at 22:31
  • 1
    The standard is explicit in its requirements, and already accurately cited in the question. Function parameters are not temporary objects. –  Nov 29 '14 at 00:48
  • @BevelLemelisk: If the functions are in-lined. Then he copy constructions to copy the parameters are elided (and thus the destructors for the parameters are also removed) then this is a good explanation and describes what you seeing. So it is a combination of inlining and aggrissive compiler optimizations to remove copies that results in the only the temporary destruction being seen. – Martin York Nov 29 '14 at 17:36
  • 1
    I don't think this completely answers the question. If you remove the copy/move elision, the function parameters are *still* being destroyed late, even though they are NOT temporaries: http://coliru.stacked-crooked.com/a/21a091103ce7a383 – dyp Nov 29 '14 at 17:51
  • You are right in that it is incomplete. I'm not sure how to fix the answer though. With the current voting, you can basically read the page top to bottom and get a good answer, i.e. each contributes a part to the puzzle. First this answer (temporaries' lifetime), then hvd's and Loki's in *why* that rule applies (plus the discussion in the comments). – marc Nov 29 '14 at 21:17
6

To elaborate from what by now can already be found in the comments:

Given int f(Logger);, when you write:

f(10);

this (conceptually) constructs a temporary Logger object, constructs the function parameter from that temporary object, calls the function, destroys the function parameter, and finally destroys the temporary object.

When you write:

f(f(10));

this (conceptually) constructs a temporary Logger object, constructs the function parameter from that temporary object, calls the function, destroys the function parameter, constructs a new temporary Logger object using the first function call's result, constructs the function parameter from that temporary object, calls the function, destroys the function parameter, and finally destroys the two temporary objects.

I'll avoid writing it out for f(f(f(10))); case.

Now, those two temporary objects can be omitted:

When certain criteria are met, an implementation is allowed to omit the copy/move construction of a class object, even if the copy/move constructor and/or destructor for the object have side effects. In such cases, the implementation treats the source and target of the omitted copy/move operation as simply two different ways of referring to the same object, and the destruction of that object occurs at the later of the times when the two objects would have been destroyed without the optimization. This elision of copy/move operations, called copy elision, is permitted in the following circumstances (which may be combined to eliminate multiple copies):

  • ...

  • when a temporary class object that has not been bound to a reference (12.2) would be copied/moved to a class object with the same cv-unqualified type, the copy/move operation can be omitted by constructing the temporary object directly into the target of the omitted copy/move

  • ...

Since the function parameter and the temporary object have the same type, a compiler is allowed to treat them as the same object. The temporary object would be destroyed at the final stage, so the lifetime of the parameter does not come into play.

However, when copy elision isn't performed, for example because you configure the compiler not to, or because there is no copy to elide in the first place (see below), then the function parameters must indeed be destroyed when you say they should be, and you must see "Destruct (...)" before the second function invocation starts in all conforming C++11 implementations.

A parameter can be constructed without a temporary by using braces: you could re-work the call as

f({f({f({10})})});

Here, each parameter is list-initialised, which in this case does not involve temporary objects, and there are no copies to elide. This must destroy the function parameters as soon as the function f returns, before f is called again, in all C++11-conforming implementations, regardless of any -felide-constructors command-line options, and the fact that compilers do not do this is an area in which they fail to conform to C++11.

It's not quite as simple as that, though: CWG issue 1880 reads:

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.

This would allow exactly what compilers do now: the parameters can be destroyed after the end of the full-expression, after the last f has returned. The exact literal text of C++11 isn't what current compilers implement.

  • Yes, it's a good point, that when copy-elision comes into play, lifetime of the parameter should be extended to satisfy standard requirements. – Bevel Lemelisk Dec 01 '14 at 09:34
  • Can you tell a few words about what happens in the low-level? Let not think about inlining, just an ordinary function call. I don't know assembler, but from my understanding, when we pass an argument by value, it should be constructed as a part of **called** function's stack frame, and, because of this, must be destroyed as a part of this function's returning mechanism. However, this is not happening, so how does it work? Do we pass a pointer, instead of argument itself, or keep arguments part of called function's stack frame in our stack until the end of full-expression? – Bevel Lemelisk Dec 01 '14 at 09:51
  • 1
    @BevelLemelisk The standard says parameter construction and destruction happens in the caller function. You're right that many ABIs let parameter destruction happen in the callee, and the way ABIs solve that, is by determining whether a type has a trivial destructor (there are also other considerations), and if not, as you suspected, don't pass the type, pass a reference to the type, to what does get constructed and destructed in the caller. See [Itanium C++ ABI - Value Parameters](https://mentorembedded.github.io/cxx-abi/abi.html#value-parameter) for one particular ABI's exact requirements. –  Dec 01 '14 at 10:55
  • So for any non-`TriviallyCopyable` type, passing it by value is the almost the same (in terms of generated assembler code) as passing it by reference (in case, of course, when temporary is created for this passing by reference)? Before this, I was pretty sure, that passing by value removes one layer of indirection here. Thank you for interesting info. – Bevel Lemelisk Dec 01 '14 at 11:22
  • 1
    @BevelLemelisk Pretty much, but the requirements are not exactly the same as those for non-trivially copyable types. A type with a trivial copy constructor and trivial destructor, but a non-trivial assignmentment operator, is not trivially copyable, but would (in that ABI) be passed the same way as trivially copyable types. –  Dec 01 '14 at 11:31
3

What you are expecting to see is:

Construct 0x7fffa42d97ff         //  Creation of temporary object
Copy construct 0xAAAAAAAAAA      //  copy constuction of parameter
Inside f
Destruct 0xAAAAAAAAAA            // destruction of parameter.
Construct 0x7fffa42d97fe
Copy construct 0xBBBBBBBBB
Inside f
Destruct 0xBBBBBBBBB
Construct 0x7fffa42d97fd
Copy construct 0xCCCCCCCCC
Inside f
Destruct 0xCCCCCCCCC
Destruct 0x7fffa42d97fd
Destruct 0x7fffa42d97fe
Destruct 0x7fffa42d97ff          // destruction of temporary

But the compiler is allowed to elide (remove) the copy construction of parameters (and their destructors) and inline the function. If you do this the only remaining objects that are constructed are the temporaries that are passed to the functions.

So if you take my result set remove the copy construction (caused by aggressive compiler optimization) you are left with the output you see in your answer.

If you want to see the output above. Then prevent the compiler from inlining the functions: See https://stackoverflow.com/a/1474050/14065

Note: In-lining is just one reason for the eliding the copy. The compiler can use a couple of others. I use the example on in-lining because it is the most easy to visualize the removal of the parameter being copied into the function.

Community
  • 1
  • 1
Martin York
  • 257,169
  • 86
  • 333
  • 562
  • 1
    I think http://coliru.stacked-crooked.com/a/21a091103ce7a383 shows that there's (also?) something else going on. I'd expect lifetime extension if there is copy elision, but I do not expect the late destruction of the function parameters. I do not know why you're referring to inlining, since AFAIK it is no exception to alter the observable behaviour (unlike copy elision). – dyp Nov 29 '14 at 18:04
  • @dyp: The function parameters are not destroyed late (as they would be created via a copy construction). They (the parameters) have been removed (elided). The only thing left to be destroyed are the temporaries. Which do not get to be destroyed until the end of the statement. – Martin York Nov 29 '14 at 18:21
  • 1
    In the live demo I have linked copy elision should be deactivated (`-fno-elide-constructors`). So the expression `f(10)` creates a temporary `Logger` direct-initialized from the `int` prvalue, then the function parameter is copy-initialized from that temporary. The temporary lives until the end of the call-fullexpression, but I believe the function parameter should be destroyed before the return / when the function returns (which has nothing to do with `Logger`, since it only operates on `int`s). – dyp Nov 29 '14 at 18:24
  • Can you get it not to inline the function. Destroying the parameters at the end is not what I would have expected unless the code had been inlined. We are now in the realm of the `AsIf` rule which allows a lot of leeway. – Martin York Nov 29 '14 at 18:27
  • 1
    The as-if rule does not allow for lifetime extension of any object when an object's lifetime ending has observable effects. –  Nov 29 '14 at 18:43
  • 1
    @hvd But copy elision allows it. That's the whole point of (an answer to) the OP's question: Since the temporary object is merged with the parameter (they're the *same* object), the object is created at the earlier of the two creation times, and destroyed at the later of the two destruction times. For reference, see 12.8/31 "the destruction of that object occurs at the later of the times when the two objects would have been destroyed without the optimization." – dyp Nov 29 '14 at 19:05
  • @dyp No disagreement there. My comment was in reply to Loki Astari's comment "We are now in the realm of the `AsIf` rule which allows a lot of leeway.": the as-if rule doesn't apply here. Other rules do. –  Nov 29 '14 at 19:06
  • @hvd: Yep. I agree life time extension should not be at play here (with the parameters). The example you link too is not what I would expect. **BUT** we are now in the realm of a different question. – Martin York Nov 29 '14 at 19:41
  • Even with `-fno-elide-constructors -fno-inline -O0` and `__attribute__ ((noinline))` for function `f`, we don't get your output on gcc, functions parameters are still destroyed in the end. – Bevel Lemelisk Dec 01 '14 at 09:58
2

It's correct behavior. It follows the First in, Last out method of destructing resources. If you had invoked the function in sequence you would get a different result.

f(10)
f(10)
f(10)

Would destruct like this:

Construct 0x7fffa42d97ff
Inside f
Destruct 0x7fffa42d97ff
Construct 0x7fffa42d97fe
Inside f
Destruct 0x7fffa42d97fe
Construct 0x7fffa42d97fd
Inside f
Destruct 0x7fffa42d97fd
Grice
  • 1,345
  • 11
  • 24
  • I agree, that when objects are destroyed at the **same** point of time, destruction should be in reverse order for construction. However, when objects are destroyed at **different** time this rule isn't applicable (as in your example). And I was pretty sure, that function parameters should be destroyed at their functions exit (which is **different** time for each of this 3 calls). – Bevel Lemelisk Nov 28 '14 at 22:47