[cfe-dev] Smart Pointer Lifetime Optimizations

Zoe Carver via cfe-dev cfe-dev at lists.llvm.org
Thu Jun 11 13:00:19 PDT 2020


> 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?

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/5a3a4254/attachment-0001.html>


More information about the cfe-dev mailing list