C++ Logo


Advanced search

Re: Some feedback on scope guards

From: Edward Catmur <ecatmur_at_[hidden]>
Date: Wed, 12 Apr 2023 23:16:42 -0300
On Wed, 12 Apr 2023 at 15:27, Andrey Semashev via Std-Discussion <
std-discussion_at_[hidden]> wrote:

> On 4/12/23 18:53, Edward Catmur wrote:
> > On Mon, 10 Apr 2023, 08:57 Andrey Semashev via Std-Discussion,
> > <std-discussion_at_[hidden]
> > <mailto:std-discussion_at_[hidden]>> wrote:
> >
> > I don't think that updating a pointer during a context switch would
> be a
> > deal breaker. The co_unhandled_exceptions() mechanism only needs to
> know
> > the current coroutine state of the current thread, it doesn't need a
> > list. If a linked list is needed to be able to return from one
> coroutine
> > to another then such a list must be already in place.
> >
> > Now that I think of it, co_unhandled_exceptions() might not require
> any
> > new pointers. The coroutine already has to keep a pointer to its
> state
> > somewhere to be able to reference its local variables. I don't see
> why
> > co_unhandled_exceptions() couldn't use that pointer.
> >
> > But how can co_unhandled_exceptions() *find* that pointer? The current
> > coroutine state is not stored in per thread memory; it's solely on the
> > stack. (The linked list that's "already in place" is the stack itself.)
> The compiler could pass this pointer as a hidden parameter to
> co_unhandled_exceptions() based on implementation-specific attribute, or
> co_unhandled_exceptions() could be a compiler intrinsic to begin with.
> Or, if the pointer is stored in a known fixed location of the parent
> stack frame, co_unhandled_exceptions() could extract it from there. All
> this is to say this task doesn't seem impossible.
> > That said, you might be able to find it when unwinding through a
> > coroutine stack frame. I'm still thinking about it, but it seems like it
> > should work. In that case you've only got overhead added to coroutine
> > creation, coroutine footprint, unwinding through a coroutine, and you've
> > doubled the stack footprint and memory access of the success/failure
> > scope guards, compared to a C++17 implementation using
> > unhandled_exceptions only.
> Not sure what you mean by doubling the stack footprint and memory access
> of the scope guards. The size of the scope guards didn't change.

I may have lost track of things here. Let's go back to your design: you're
adding a co_uncaught_exceptions() counter that's initialized from
uncaught_exceptions() when a coroutine is constructed and stored in
per-coroutine memory. (We'll assume that can be arranged without breaking
ABI, since the coroutine allocation - if there is one - is provided by the
runtime.) The co_uncaught_exceptions() counter is incremented when
unwinding encounters the coroutine, and decremented when a coroutine catch
block is entered - this is fine, unwinding has access to the relevant
information. The scope_success and scope_failure guards record the value of
co_uncaught_exceptions() on construction, and consult it on destruction; if
it is greater than the recorded value (it should never be lesser) then a
failure condition obtains. OK, I can see how there is no increased
footprint, since the recorded co_uncaught_exceptions() value replaces the
recorded uncaught_exceptions() value, my bad.

But *how* do scope_success and scope_failure access the current
coroutine's co_uncaught_exceptions() counter? Obviously it's not a problem
if they're complete automatic objects of the coroutine frame, but
otherwise? e.g. if they're subobjects or dynamically allocated? I'd think
this would require stack walking (non-destructive unwinding), which is
hugely expensive, or constructing a thread-local linked list of coroutines,
which has continuous overhead.

And what for scope_success and scope_failure that aren't constructed from
within coroutines at all? How far do they look to determine to use
uncaught_exceptions() instead?

And what if a dynamically allocated scope_success/scope_failure is
constructed in one coroutine, but then has ownership transferred to another
stack (which may or may not be a coroutine)?

> But doesn't adding the information for co_unhandled_exceptions() to
> > coroutine state break abi?
> It should be implementable without breaking the ABI, if the coroutine
> state is allocated and initialized by the runtime (and I assume it
> should be the case). For example, you could put the added counter before
> the data used by the coroutine (parameters and locals), at a negative
> offset known to the runtime.

HALO coroutines aren't allocated... but maybe that's OK, since they're
always resumed from the same stack context. (Are they?)

> When I say "the destruction of the scope guard is caused by a stack
> > unwinding procedure" I mean literally that the stack unwinding
> procedure
> > is present higher up in the call stack from the scope guard
> destructor.
> > I'm choosing this criteria because that's what makes most sense to
> me in
> > relation to the expected behavior of the scope guards.
> >
> > That doesn't work. Having an unwind caused destructor above in the stack
> > from a scope guard destructor does not always mean that the scope guard
> > is in a failure state. It feels like we're going round in circles here.
> A scope guard destructor would conceal this information by setting the
> uncaught exceptions counter to zero, as described below.

Is this a revision to your design, an extension, or an alternate design?
I'm having trouble keeping track.

> > In a
> > > perfect world, I would prefer to have scope guard-like
> > functionality in
> > > the core language (but not in the form of try/finally from
> Java).
> > >
> > > Even if we had scope guard in the core language, its actions would
> be
> > > executed during stack unwinding, no?
> >
> > Yes, but a core language feature could have better syntax and better
> > defined semantics as it wouldn't have to rely on
> unhandled_eceptions().
> >
> > As could a library feature with compiler support, and that wouldn't
> > require new syntax.
> A new syntax could offer more benefits, like not having to name the
> scope guard, for example. It would also make the discussion re.
> unhandled_exceptions() moot as it would simply not use it internally.

Well, yes. The argument against new syntax has always been that it is
unnecessary since we can implement scope guards in library. If it turns out
that scope_success and scope_failure can't be implemented without either
incorrect behavior, considerable overhead, or heavy restrictions on use,
that might swing things back the other way.

Received on 2023-04-13 02:16:58