[cfe-dev] Smart Pointer Lifetime Optimizations

John McCall via cfe-dev cfe-dev at lists.llvm.org
Thu Jun 11 12:10:50 PDT 2020


On 11 Jun 2020, at 12:57, Richard Smith wrote:
> On Wed, 10 Jun 2020 at 19:52, John McCall via cfe-dev <
> cfe-dev at lists.llvm.org> wrote:
>
>> On 10 Jun 2020, at 18:30, Richard Smith wrote:
>>
>> On Wed, 10 Jun 2020 at 13:06, John McCall via cfe-dev <
>> cfe-dev at lists.llvm.org> wrote:
>>
>> On 10 Jun 2020, at 15:55, Richard Smith wrote:
>>
>> On Wed, 10 Jun 2020 at 12:18, John McCall via cfe-dev <
>> cfe-dev at lists.llvm.org> wrote:
>>
>> On 10 Jun 2020, at 14:32, Richard Smith wrote:
>>
>> On Mon, 8 Jun 2020 at 19:52, John McCall via cfe-dev
>> <cfe-dev at lists.llvm.org>
>> wrote:
>>
>> It definitely changes observable semantics, but it’s not
>> *obviously*
>> non-conforming; [expr.call]p7 gives us a lot of flexibility here:
>>
>> 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.
>>
>> This is the non-conformance I'm referring to:
>> https://godbolt.org/z/cgf5_2
>>
>> Even given [expr.call]p7, we are still required to destroy
>> automatic-storage-duration objects in reverse construction order by
>> [stmt.jump]p2:
>>
>> "On exit from a scope (however accomplished), objects with automatic
>> storage duration (6.7.5.3) that have been constructed in that scope
>> are
>> destroyed in the reverse order of their construction."
>>
>> Don’t temporaries not have automatic storage duration formally?
>>
>> The intent is that they don't; we have a longstanding open issue to
>> introduce a notion of "full-expression storage duration" to describe
>> temporary objects. But in the absence of such a language change, it's
>> unclear which utterances about "automatic storage duration" apply to
>> temporaries.
>>
>> But in any case, I think that's immaterial here, because function
>> parameters are local variables, not temporary objects, and do have
>> automatic storage duration even in the hypothetical world where 
>> there's a
>> different storage duration for temporaries.
>>
>> That’s why [class.temporary]p7 has to spell out the interordering
>>
>> of destruction of lifetime-extended temporaries.
>>
>> [expr.call]p7 is the most specific statement about the destruction
>> of parameters. Under normal principles of interpretation, it should
>> take priority.
>>
>> Well, p7 says nothing about the relative ordering of parameter
>> destructions, only the points where such destruction may occur.
>>
>> Not relative with each other, but I think it has to be understood
>> as allowing differences relative to temporaries created by the 
>> calling
>> full-expression.
>>
>> Oh, definitely. Prior to C++17, that's a problem (as described in
>> http://itanium-cxx-abi.github.io/cxx-abi/argument-destruction.pdf). 
>> In
>> C++17 and later, though, there is never a case where we elide a copy 
>> of a
>> temporary into a parameter, so we never have a function parameter 
>> that
>> might need to outlive its call due to the "longer of the two 
>> lifetimes"
>> rule. The new model is that a prvalue of class type simply 
>> initializes its
>> target in-place, without even notionally creating and copying from a
>> temporary.
>>
>> That’s great, but we can still have temporaries created during the
>> evaluation of other arguments, and callee-destroy does still mean 
>> that
>> they won’t be destroyed in reverse order of construction, and that 
>> has to
>> be okay.
>>
>> For example:
>>
>> struct A {
>>   A(const std::string &);
>>   ~A();
>> };
>>
>> void foo(A x, A y);
>>
>> void test() {
>>   foo(A(“a”), A(“b”));
>> }
>>
>> Assuming left-to-order argument evaluation order, the caller 
>> constructs:
>> 1. the std::string for “a”
>> 2. the parameter x
>> 3. the std::string for “b”
>> 4. the parameter y
>>
>> If parameters are destroyed at the end of the call, we’ll destroy 
>> them
>> in the order 4,2,3,1 instead of 4,3,2,1. That has to be okay.
>>
> Yes, that must be OK. I think in general there is no requirement that
> full-expression-storage-duration objects and 
> automatic-storage-duration
> objects are destroyed in reverse construction order relative to each 
> other.

Right.

> Another example of that:
>
> A make_a(B &&);
> A &a = make_a(B()); // full-expression-storage-duration B is destroyed 
> long
> before automatic-storage-duration A.

Right.

The basic problem here is that storage duration stops being a
particularly good proxy for lifetime / destruction order when you
start talking about locals.  The real categories seem to be: dynamic,
global, thread, local-scope, full-expression, and (optionally) 
parameters.

Hmm. There was a discussion about throwing destructors in bugzilla a
few days ago, and we were talking about how to properly destroy 
initialized
objects if a destructor throws during the teardown of the 
full-expression.
It just occurred to me that this is *much* thornier than I’d given it 
credit
for because you can have arbitrarily many initialized objects and they 
can
be arbitrarily intermingled with the temporaries, because of lifetime
extension of `std::initializer_list` and other recursive/branching
extensions.  So e.g. if you have:

```
struct Temp {
   Temp(const char *);
   ~Temp() noexcept(false);
};

struct Element {
   Element(Temp);
   ~Element();
};

std::initializer_list<Element> &&list = {
   { Temp(“a”) },
   { Temp(“b”) },
   { Temp(“c”) }
};
```

Then the normal destruction order is:

   Temp_c, Temp_b, Temp_a, /*end of scope*/, Element_c, Element_b, 
Element_a

But if Temp_c’s destructor throws, then I guess the exceptional 
destructor order is:

   Element_c, Element_b, Temp_b, Element_a, Temp_a

Unless there’s an argument that the whole `std::initializer_list` 
becomes
a single object at some point?  This doesn’t affect normal aggregate
list-initialization of an object because each initialization is a 
separate
full-expression, but I don’t think that’s true of a 
`std::initializer_list`
that’s just in the middle of an expression, right?

>> This (especially the second bullet) would be an ABI change for 
>> existing
>> users of [[trivial_abi]]. (Alternative simpler rule: if any parameter 
>> is
>> callee-cleanup then all parameter destruction happens in the callee. 
>> I'm
>> not sure if that's better or worse, but it seems simpler.)
>>
>> It does, yeah. But I guess we’d still need two entrypoints to get 
>> the
>> ordering right? Seems really unfortunate.
>>
> Yes, but I think we would only need two entry points for non-member
> `operator$=` functions that use callee cleanup. I think that's rare 
> enough
> that the cost wouldn't be prohibitive.

Right, good point.  As long as the ABI divergence is specific to 
functions
with callee cleanup, it’s acceptable.

John.
-------------- next part --------------
An HTML attachment was scrubbed...
URL: <http://lists.llvm.org/pipermail/cfe-dev/attachments/20200611/c1fc9a46/attachment.html>


More information about the cfe-dev mailing list