[cfe-dev] Smart Pointer Lifetime Optimizations

John McCall via cfe-dev cfe-dev at lists.llvm.org
Thu Jun 11 13:03:41 PDT 2020


On 11 Jun 2020, at 16:00, Zoe Carver wrote:
> > 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.
>
> Is it too late to say that the destructor of objects marked with
> `trivial_abi` must be noexcept?

It’s not too late, but it seems arbitrary, and it doesn’t make any
difference; the only way to actually define these concerns away would
be to make *all* destructors noexcept.

John.

>
> On Thu, Jun 11, 2020 at 12:10 PM John McCall <rjmccall at apple.com> 
> wrote:
>
>> 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/4af9bc91/attachment.html>


More information about the cfe-dev mailing list