C++ Logo

std-discussion

Advanced search

Re: Some feedback on scope guards

From: Edward Catmur <ecatmur_at_[hidden]>
Date: Thu, 13 Apr 2023 10:03:59 -0300
On Thu, 13 Apr 2023, 06:35 Andrey Semashev via Std-Discussion, <
std-discussion_at_[hidden]> wrote:

> On 4/13/23 05:16, Edward Catmur wrote:
> >
> >
> > On Wed, 12 Apr 2023 at 15:27, Andrey Semashev via Std-Discussion
> > <std-discussion_at_[hidden]
> > <mailto: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]>
> > > <mailto: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.
> >
> > 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.

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

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

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

>

Received on 2023-04-13 13:04:14