C++ Logo

std-discussion

Advanced search

Re: Some feedback on scope guards

From: Edward Catmur <ecatmur_at_[hidden]>
Date: Thu, 13 Apr 2023 15:42:48 -0300
On Thu, 13 Apr 2023, 14:26 Andrey Semashev via Std-Discussion, <
std-discussion_at_[hidden]> wrote:

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

Suspending a coroutine, which returns to the caller, doesn't involve
another coroutine, so there's nothing to connect the two. Yielding a result
to another coroutine does involve that other coroutine, but via the
coroutine library, not the compiler, and as far as the stack is concerned
the two are siblings, not in an ancestor - descendant relationship.

> > 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).


Which is why a linked list is required. When a coroutine is suspended and
control returns to its non-coroutine caller, neither have any idea of what
other coroutine may be above them on the stack.

Anyway, Lee has pointed out that coroutines may as well just update and
restore the uncaught_exceptions counter directly on resume and suspend.

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

And how would you define that case? I've tried and I can't come up with
anything watertight.

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


I'm not sure. It feels like it is, but I haven't yet seen a formal
specification or sample implementation to pick apart.

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

This sounds to me like it's an alternate design.

>

Received on 2023-04-13 18:43:04