C++ Logo

std-discussion

Advanced search

Re: Some feedback on scope guards

From: Andrey Semashev <andrey.semashev_at_[hidden]>
Date: Thu, 13 Apr 2023 12:35:30 +0300
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.

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

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

Received on 2023-04-13 09:35:35