C++ Logo

std-discussion

Advanced search

Re: Some feedback on scope guards

From: Andrey Semashev <andrey.semashev_at_[hidden]>
Date: Thu, 13 Apr 2023 20:25:30 +0300
On 4/13/23 16:03, Edward Catmur wrote:
>
> On Thu, 13 Apr 2023, 06:35 Andrey Semashev via Std-Discussion,
> <std-discussion_at_[hidden]
> <mailto:std-discussion_at_[hidden]>> wrote:
>
> On 4/13/23 05:16, Edward Catmur wrote:
> >
> > 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.
>
> I think you are confusing the storage used for the scope guard object
> with the stack frame. co_uncaught_exceptions() doesn't need or use the
> scope guard object, it doesn't care where it is allocated or whether it
> exists at all. What co_uncaught_exceptions() *may* need is its caller's
> stack frame, if it is implemented in such a way that it uses the stack
> frame to obtain the pointer to the coroutine state (or to discover
> whether it is a coroutine at all).
>
> Now that I think of it more, perhaps using the caller's stack frame is
> not a viable idea after all, since the immediate caller of
> co_uncaught_exceptions() may not be a coroutine, but a normal function
> called within a coroutine. However, as I noted earlier, there are other
> possible implementations, including not involving TLS. But using TLS to
> store a pointer to the current coroutine state would be the simplest
> solution, of course.
>
> Right. But that would require a linked list, since otherwise there's no
> way to restore the previous value when a coroutine suspends or returns.

If so then the linked list must already exist, because you can return
from coroutines today, and that requires the compiler to be able to
switch between coroutine states. Or am I missing something?

> > 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?
>
> The scope guard would always use co_uncaught_exceptions(). When called
> not within a coroutine (meaning, there are no coroutines higher up the
> stack), co_uncaught_exceptions() would be equivalent to the
> uncaught_exceptions() we currently have.
>
> And how does co_uncaught_exceptions() know that there are no coroutines
> higher up the stack? It sounds like a thread local linked list is a
> necessity.

It doesn't need to be a list, at least not a new list in addition to
what there is in place today. Again, co_uncaught_exceptions() only needs
to know the *current* coroutine state, or that there isn't one (which
means that there are no coroutines higher up the stack). Some
implementations could optimize this by making main() and thread startup
functions initialize a root state with an ABI similar to a coroutine, so
that co_uncaught_exceptions() is always able to transparently use a
state (either the root state or a coroutine state).

> > 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)?
>
> In that case, the cached number of uncaught exceptions may become not
> actual, depending on what kind of transfer you make. Yes, this may break
> scope_success/scope_fail, unfortunately. I'd be willing to mark such use
> of scope_success/scope_fail UB, as this is arguably a case of the user
> explicitly doing something incompatible with those scope guards' design,
> as opposed to using scope guards in the conventional way within
> coroutines.
>
> Right, so this design is still fairly fragile. Which behaviors would you
> define, beyond the absolute minimum of scope guards as complete
> automatic objects?

The only limitation identified so far is the one you pointed out above,
which is extending scope_success/scope_fail lifetime beyond its scope of
construction, so that the captured uncaught exception count becomes not
actual. This use case I would agree to leave unsupported, although with
use of co_set_uncaught_exceptions() it could still be supported. See below.

Other than this, the behavior seems to be well defined in all cases we
have considered so far, is it not?

> > > 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.
>
> No, it's the same design. Or you could say it's an evolution of the same
> design during the discussion. It's all about co_uncaught_exceptions()
> and co_set_uncaught_exceptions(), the latter being used for concealing
> the pending exceptions from the scope guard action, as I have shown
> earlier.
>
> Is this a necessary part of the design or can it be omitted? Which
> scenarios does it help with?

It would allow to differentiate the destruction code from the arbitrary
code that is called from a destructor, e.g. by means of a scope guard.

If the whole program follows the protocol of setting the uncaught
exception count to zero before running non-destruction code within
destructors, scope_success/scope_fail could rely on the fact that
`co_uncaught_exceptions() > 0` always means failure. First, it would
allow to eliminate one call to co_uncaught_exceptions() (on the scope
guard construction). Second, and more importantly, it would allow to
solve the issue you pointed out above - what happens if you move
scope_fail out of the scope of its construction? The answer would be, it
would still invoke its action if at the point of its destruction (which
would correspond to where it was moved to) there is an exception in
flight, as indicated by the `co_uncaught_exceptions() > 0` condition.

I understand that following the protocol of setting uncaught exception
count to zero in every scope guard-like type's destructor might be a lot
to ask for. I also understand that the use case of moving
scope_success/scope_fail out of its scope of construction is
unconventional. Which is why I'm not insisting on supporting this use
case and co_set_uncaught_exceptions(). The proposed
co_uncaught_exceptions() would still be useful for solving the problem
with coroutines.

Received on 2023-04-13 17:25:55