Date: Wed, 18 May 2022 01:31:08 +0100
On Sat, 7 May 2022 at 07:03, Thiago Macieira via Std-Proposals <
std-proposals_at_[hidden]> wrote:
> On Thursday, 5 May 2022 18:37:42 PDT Edward Catmur via Std-Proposals wrote:
> > In practical terms, what happens if we have a type T with user-defined
> > destructor, and to release some resource ~T accesses members x and y,
> with
> > x having throwing (move-plus-destroy) relocate?
>
> If x's relocator throws, then when that exception is thrown, both source
> and
> destination objects have no lifetime. The destination's has never started
> and
> the source has irrevocably ended the moment the relocator started. There's
> no
> going back.
>
The wrinkle here is that for a relocator synthesized from move+destroy, the
destination and source lifetime do overlap, observably. So one argument
might be that in that case the relocator should have a (weak) exception
guarantee that the source lifetime persists. However, that would be
inconsistent with the behavior in the case of a relocator proper, so I
agree that the synthesized behavior should be to destroy the source object
if an exception is thrown during the move sub-operation.
> > Suppose x's destructor
> > throws after dst.x has been move-constructed from src.x;
>
> This condition is not possible. The relocator is responsible for
> transferring
> the lifetime. If it throws -- regardless of it did so explicitly or
> because a
> function it called threw -- then dst.x is not constructed. Unwinding
> happens
> and dst.x's lifetime rolls back.
>
Yes, you're correct. However note that if we add one more data member z, we
can end up in this situation: x successfully relocates, y throws, but to
release the resource T needs x and z.
I agree it's unacceptable, but what's unacceptable is writing throwing
> moves
> and throwing destructors. Whoever writes those is accepting pain.
>
> Relocators, like destructors, should be by default noexcept(true). Making
> them
> noexcept(false) should be an intentional and explicit choice. In turn, it
> means that if the choice wasn't made, then calling a function that throws
> from
> that relocator and failing to catch will cause std::terminate(), as you
> proposed.
>
I'm a bit worried about this, actually. We got away with making destructor
noexcept by default in C++11 since hardly anyone was writing
actually-throwing destructors, because they were already bad news, prone to
program termination if they were called during stack unwinding (which is
pretty much unavoidable).
But actually (or at least potentially) throwing move constructors are
reasonably common, so if we wrap them in an automatically generated
noexcept-by-default synthesized special member function/operator, we're
running the risk of destroying the credibility of the noexcept guarantee -
we do not want to find that people (e.g. container authors) are avoiding
calling relocate because they're worried about std::terminate from an
automatically noexcept relocator and are instead performing move+destroy
manually.
So I think that given that a relocator may (possibly invisibly) call a
possibly-throwing move constructor, it cannot be noexcept by default.
Instead, I would say that if a class has a user-defined destructor (and so
is suspected of owning resources directly), and if at least one of its
bases-and-members has throwing relocate, then its relocator cannot be
defaulted (declared or defined as defaulted) but must be user-defined (or
deleted). Also, a user-defined relocator (i.e. one not declared as
defaulted or deleted) should not be noexcept if unspecified; either they
should be noexcept(false) if unspecified, or a noexcept specification
should be required.
Finally, to be able to avoid leaks from classes with user-defined
destructor and at least one throwing relocate base-or-member, it should be
possible to add syntax to a user-defined relocator to recover from an
exception throw by a base-or-member relocate. My current idea is adding a
parenthesized init-statement to a function-try-block:
operator relocate(T src)
try (auto p = src.y)
/* base-or-member-initializers */
{
/* relocator function-body */
}
catch (...)
{
delete p;
}
If any of the base-or-member-initializers throw (noting that the default is
to relocate from the corresponding base-or-member on the source object,
which may throw or be synthesized from throwing move+destroy), the
preceding base-or-members are destroyed on the target object and the
succeeding base-or-members on the source object; if the relocator
function-body throws the base-or-members are destroyed on the target
object; then the function-catch-block is invoked, then the init-statement's
variable is destroyed, then the exception is allowed to continue (with both
source and target object assumed destroyed). If nothing throws, then the
target object's lifetime begins and then the init-statement's variable is
destroyed; if that destructor throws, the target object's destructor is
invoked.
std-proposals_at_[hidden]> wrote:
> On Thursday, 5 May 2022 18:37:42 PDT Edward Catmur via Std-Proposals wrote:
> > In practical terms, what happens if we have a type T with user-defined
> > destructor, and to release some resource ~T accesses members x and y,
> with
> > x having throwing (move-plus-destroy) relocate?
>
> If x's relocator throws, then when that exception is thrown, both source
> and
> destination objects have no lifetime. The destination's has never started
> and
> the source has irrevocably ended the moment the relocator started. There's
> no
> going back.
>
The wrinkle here is that for a relocator synthesized from move+destroy, the
destination and source lifetime do overlap, observably. So one argument
might be that in that case the relocator should have a (weak) exception
guarantee that the source lifetime persists. However, that would be
inconsistent with the behavior in the case of a relocator proper, so I
agree that the synthesized behavior should be to destroy the source object
if an exception is thrown during the move sub-operation.
> > Suppose x's destructor
> > throws after dst.x has been move-constructed from src.x;
>
> This condition is not possible. The relocator is responsible for
> transferring
> the lifetime. If it throws -- regardless of it did so explicitly or
> because a
> function it called threw -- then dst.x is not constructed. Unwinding
> happens
> and dst.x's lifetime rolls back.
>
Yes, you're correct. However note that if we add one more data member z, we
can end up in this situation: x successfully relocates, y throws, but to
release the resource T needs x and z.
I agree it's unacceptable, but what's unacceptable is writing throwing
> moves
> and throwing destructors. Whoever writes those is accepting pain.
>
> Relocators, like destructors, should be by default noexcept(true). Making
> them
> noexcept(false) should be an intentional and explicit choice. In turn, it
> means that if the choice wasn't made, then calling a function that throws
> from
> that relocator and failing to catch will cause std::terminate(), as you
> proposed.
>
I'm a bit worried about this, actually. We got away with making destructor
noexcept by default in C++11 since hardly anyone was writing
actually-throwing destructors, because they were already bad news, prone to
program termination if they were called during stack unwinding (which is
pretty much unavoidable).
But actually (or at least potentially) throwing move constructors are
reasonably common, so if we wrap them in an automatically generated
noexcept-by-default synthesized special member function/operator, we're
running the risk of destroying the credibility of the noexcept guarantee -
we do not want to find that people (e.g. container authors) are avoiding
calling relocate because they're worried about std::terminate from an
automatically noexcept relocator and are instead performing move+destroy
manually.
So I think that given that a relocator may (possibly invisibly) call a
possibly-throwing move constructor, it cannot be noexcept by default.
Instead, I would say that if a class has a user-defined destructor (and so
is suspected of owning resources directly), and if at least one of its
bases-and-members has throwing relocate, then its relocator cannot be
defaulted (declared or defined as defaulted) but must be user-defined (or
deleted). Also, a user-defined relocator (i.e. one not declared as
defaulted or deleted) should not be noexcept if unspecified; either they
should be noexcept(false) if unspecified, or a noexcept specification
should be required.
Finally, to be able to avoid leaks from classes with user-defined
destructor and at least one throwing relocate base-or-member, it should be
possible to add syntax to a user-defined relocator to recover from an
exception throw by a base-or-member relocate. My current idea is adding a
parenthesized init-statement to a function-try-block:
operator relocate(T src)
try (auto p = src.y)
/* base-or-member-initializers */
{
/* relocator function-body */
}
catch (...)
{
delete p;
}
If any of the base-or-member-initializers throw (noting that the default is
to relocate from the corresponding base-or-member on the source object,
which may throw or be synthesized from throwing move+destroy), the
preceding base-or-members are destroyed on the target object and the
succeeding base-or-members on the source object; if the relocator
function-body throws the base-or-members are destroyed on the target
object; then the function-catch-block is invoked, then the init-statement's
variable is destroyed, then the exception is allowed to continue (with both
source and target object assumed destroyed). If nothing throws, then the
target object's lifetime begins and then the init-statement's variable is
destroyed; if that destructor throws, the target object's destructor is
invoked.
Received on 2022-05-18 00:31:21