Date: Thu, 18 Aug 2022 14:37:58 +0200
On Thu, Aug 18, 2022 at 12:35 AM Edward Catmur <ecatmur_at_[hidden]>
wrote:
> Ah, I'm talking about swap implemented memberwise, figuratively:
>
> void T::swap(T& rhs) {
> using std::swap;
> swap(a_, rhs.a_);
> swap(b_, rhs.b_);
> swap(c_, rhs.c_);
> // ...
> }
>
> So, after inlining, `y = reloc x;` becomes:
>
> swap(y.a_, x.a_);
> swap(y.b_, x.b_);
> swap(y.c_, x.c_);
> // ...;
> x.~T();
>
> Usually each data member swap would be inlined, and the compiler would
> observe that it just needs to swap two chunks of memory, with exceptions
> for self-referential types.
>
> It's certainly not more efficient than destroy-and-rebuild; and it's more
> work (at present, you have to write out a swap function by hand). But it
> is safe against exceptions.
>
The problem is then, as you pointed out, we don't have anything at the
moment to default generate a memberwise swap implementation. I've seen this
proposal P0198R0 (
https://www.open-std.org/jtc1/sc22/wg21/docs/papers/2016/p0198r0.pdf), but
I don't know what kind of reception it got.
What about a mixed approach, that could take the best of both worlds? T&
operator=(T src) = reloc; would generate the assignment operator with the
same aliasing as with the reloc ctor. This bit is important, as it means
that the source object will be leaving the assignment operator in a
destructed state (and as such its destructor will not be called, as if by
the reloc ctor).
Then I see two possible implementations:
1. If T has a noexcept reloc ctor and dtor, then destroy-and-relocate is
performed.
2. Otherwise, we perform a memberwise destroy-and-relocate by calling
the prvalue-assignment operator on each subobject:
- The prvalue-assignment may be user-provided (in which case a
temporary copy of the subobject is passed) or generated by =reloc.
- This generated code shall handle exceptions in a way that is
similar to constructor initializers: if any subobject prvalue-assignment
operator throws, then the destructor of each source subobject
that has not
been relocated will be called before the exception is propagated. This
should be exception safe, as if done by the copy-and-swap idiom.
- The only problem I foresee is if T has a user-provided destructor,
in which case the destructor body will not be called on the source object
in case an exception is thrown. For that reason, that second
implementation
should be deleted if T has a user-provided dtor.
Let's illustrate that with an example:
struct T : B
{
D _d;
T& operator=(T src) = reloc;
/* If T has a noexcept reloc ctor and noexcept dtor then equivalent to:
{
std::destroy_at(this);
return *std::relocate_at(&src, this);
}
*/
/* Otherwise if T has no user-provided destructor then equivalent to:
{
try {
B::operator=(src); // call B prvalue-assignment operator.
// The syntax is incorrect as src is an lvalue but we don't
have
// a syntax for it, especially considering reloc-aliasing
}
catch (...)
{
std::destroy_at(&src._d); // destruct all source subobjects
that have not been relocated
throw;
}
_d = src._d; // call D prvalue-assignment operator
return *this;
}
*/
/* Otherwise, T& operator=(T) is deleted. */
};
wrote:
> Ah, I'm talking about swap implemented memberwise, figuratively:
>
> void T::swap(T& rhs) {
> using std::swap;
> swap(a_, rhs.a_);
> swap(b_, rhs.b_);
> swap(c_, rhs.c_);
> // ...
> }
>
> So, after inlining, `y = reloc x;` becomes:
>
> swap(y.a_, x.a_);
> swap(y.b_, x.b_);
> swap(y.c_, x.c_);
> // ...;
> x.~T();
>
> Usually each data member swap would be inlined, and the compiler would
> observe that it just needs to swap two chunks of memory, with exceptions
> for self-referential types.
>
> It's certainly not more efficient than destroy-and-rebuild; and it's more
> work (at present, you have to write out a swap function by hand). But it
> is safe against exceptions.
>
The problem is then, as you pointed out, we don't have anything at the
moment to default generate a memberwise swap implementation. I've seen this
proposal P0198R0 (
https://www.open-std.org/jtc1/sc22/wg21/docs/papers/2016/p0198r0.pdf), but
I don't know what kind of reception it got.
What about a mixed approach, that could take the best of both worlds? T&
operator=(T src) = reloc; would generate the assignment operator with the
same aliasing as with the reloc ctor. This bit is important, as it means
that the source object will be leaving the assignment operator in a
destructed state (and as such its destructor will not be called, as if by
the reloc ctor).
Then I see two possible implementations:
1. If T has a noexcept reloc ctor and dtor, then destroy-and-relocate is
performed.
2. Otherwise, we perform a memberwise destroy-and-relocate by calling
the prvalue-assignment operator on each subobject:
- The prvalue-assignment may be user-provided (in which case a
temporary copy of the subobject is passed) or generated by =reloc.
- This generated code shall handle exceptions in a way that is
similar to constructor initializers: if any subobject prvalue-assignment
operator throws, then the destructor of each source subobject
that has not
been relocated will be called before the exception is propagated. This
should be exception safe, as if done by the copy-and-swap idiom.
- The only problem I foresee is if T has a user-provided destructor,
in which case the destructor body will not be called on the source object
in case an exception is thrown. For that reason, that second
implementation
should be deleted if T has a user-provided dtor.
Let's illustrate that with an example:
struct T : B
{
D _d;
T& operator=(T src) = reloc;
/* If T has a noexcept reloc ctor and noexcept dtor then equivalent to:
{
std::destroy_at(this);
return *std::relocate_at(&src, this);
}
*/
/* Otherwise if T has no user-provided destructor then equivalent to:
{
try {
B::operator=(src); // call B prvalue-assignment operator.
// The syntax is incorrect as src is an lvalue but we don't
have
// a syntax for it, especially considering reloc-aliasing
}
catch (...)
{
std::destroy_at(&src._d); // destruct all source subobjects
that have not been relocated
throw;
}
_d = src._d; // call D prvalue-assignment operator
return *this;
}
*/
/* Otherwise, T& operator=(T) is deleted. */
};
Received on 2022-08-18 12:38:10