Date: Fri, 19 Aug 2022 13:29:17 +0100
On Fri, 19 Aug 2022 at 10:35, Sébastien Bini <sebastien.bini_at_[hidden]>
wrote:
> On Thu, Aug 18, 2022 at 6:49 PM Edward Catmur <ecatmur_at_[hidden]>
> wrote:
>
>> Actually, I think that aliasing on relocating assignment is not feasible.
>> The reason is ABI: we can currently write a (non-special) assignment
>> operator with the signature we want to use for relocating assignment, so
>> that ABI cannot change. In particular, we can declare the relocating
>> assignment operator in a header and then later define it out-of-line in a
>> source file, possibly by default: `T& T::operator=(T) = default;`. It would
>> be inelegant if the behavior of that assignment operator were required to
>> be different dependent on whether it was defaulted in-class or out-of-class.
>>
>
> Yes, for the aliasing to work, the = reloc part must appear in the
> declaration. I admit this is not ideal, and we should split the aliasing
> part from the definition.
>
> As such, we could write:
>
> reloc T& operator=(T); // = default; or user-defined in implementation
> file
>
> "reloc" here would turn on the aliasing magic. It would live in the same
> space as "virtual" and "static", and would not be part of the function
> signature. It needs not to be rewritten in the function definition. "reloc"
> could only be added on the prvalue-assignment operator, and users would not
> be allowed to take the address of such a function (as it is a special
> function with a special ABI).
>
OK, but this seems like quite a lot of work for what is after all just an
optimization - eliminating one call to the relocating constructor, that
could have been elided anyway?
And as sugar, we could add that if the prvalue-assignment operator is
> defaulted in its declaration then the reloc aliasing is implicit. This
> works as today there is no default code generation for the
> prvalue-assignment operator.
>
> // in class declaration:
> T& operator=(T) = default: // reloc is implicitly added
>
Sure, that's fine since it gets inlined anyway; there is no symbol to have
ABI.
That being said, now lies the problem on how to write a custom
> implementation for this assignment operator. For the relocation ctor, we
> worked around this by saying that omitted subobject initializers would
> automatically call the appropriate reloc ctor of said subobject. We cannot
> do that magic for the reloc-assign, as there is no initializer list.
> However, I believe that std::relocate can come to the rescue here:
>
> class T : B
> {
> D _d;
> reloc T& operator=(T rhs)
> { // exception safety is omitted for the example
> B::operator=(std::relocate(static_cast<B*>(&rhs)));
> _d = std::relocate(&rhs._d);
> return *this;
> }
> };
>
> std::relocate is a nice fit here as, thanks to the aliasing, we know that
> rhs dtor will not be called. In the same manner, std::relocate could also
> be used to initialize subobjects in the reloc ctor.
>
Sorry, no. Aliasing doesn't prevent the destructor being called, it just
means that the parameter is the same object as (has the same address as)
the argument. It still has to be (will be) destroyed, either by the
destructor or by the relocating constructor. If you want to write code
like that, you need to relocate rhs into a buffer (placement-new-relocate)
or union.
As a side note, I wonder to what extent std::relocate could not be a
> special function that, as reloc, simply turns some address into a prvalue,
> without actually performing any relocation. That could allow nice
> optimization bits in user-defined reloc ctor or assignment operator.
>
Definitely, if that can be made to work. That's really nice; it supports my
idea of relocation being conceptually a change in value category.
Sure, but the user is capable of doing this themselves?
>>
>> 1. T& operator=(T rhs) { static_assert(noexcept(...)); this->~T(); return
>> *new (this) T(reloc rhs); }
>> 2. T& operator=(T) = default; // ill-formed if ~T() is user-provided
>>
>> Yes, they don't get automatic selection between the two, but I don't
>> really see that as an issue; it'd be quite surprising.
>>
>
> I feel mixed with having = default; doing only memberwise assignments:
>
> - This will force all users that have a user-provided dtor to write
> their own reloc-assignment operator, while it is likely that their reloc
> ctor and dtor are noexcept :/
> - If = default also generates destroy-and-relocate then it enables
> nice recursion. Without this some classes will need to write their own
> reloc-assignment operator while the default generated destroy-and-relocate
> would fit like a glove. Take unique_ptr for instance. This one has a
> legitimate user-provided dtor, and as such is not eligible to
> memberwise-reloc-assign (which is good, as it would otherwise leak its own
> resource). As a consequence std::unique_ptr would need to provide a
> reloc-assign definition, which will ultimately, just free its resource and
> steal that of the source object. Something that the default generated
> destroy-and-relocate would do for free.
>
> My point is that, if = default (a) generates destroy-and-relocate, and (b)
> falls back to memberwise-reloc-assignments, then users will almost never
> need to write any implementation. Think of case (a) as the base/halting
> cases of any recursive function.
>
Against this, there's the inconsistency with the other special assignment
operators (copy and move). Consistency is important for teachability and
to ensure that users have the correct mental model. Unless you're
proposing to change those as well...
I feel that it's OK to expect library authors (a small proportion of users
in general) to write `T& operator=(T rhs) noexcept { this->~T(); return
*new (this) T(reloc rhs); }`. It's only 30 tokens, against 10 to explicitly
default the relocating assignment operator.
wrote:
> On Thu, Aug 18, 2022 at 6:49 PM Edward Catmur <ecatmur_at_[hidden]>
> wrote:
>
>> Actually, I think that aliasing on relocating assignment is not feasible.
>> The reason is ABI: we can currently write a (non-special) assignment
>> operator with the signature we want to use for relocating assignment, so
>> that ABI cannot change. In particular, we can declare the relocating
>> assignment operator in a header and then later define it out-of-line in a
>> source file, possibly by default: `T& T::operator=(T) = default;`. It would
>> be inelegant if the behavior of that assignment operator were required to
>> be different dependent on whether it was defaulted in-class or out-of-class.
>>
>
> Yes, for the aliasing to work, the = reloc part must appear in the
> declaration. I admit this is not ideal, and we should split the aliasing
> part from the definition.
>
> As such, we could write:
>
> reloc T& operator=(T); // = default; or user-defined in implementation
> file
>
> "reloc" here would turn on the aliasing magic. It would live in the same
> space as "virtual" and "static", and would not be part of the function
> signature. It needs not to be rewritten in the function definition. "reloc"
> could only be added on the prvalue-assignment operator, and users would not
> be allowed to take the address of such a function (as it is a special
> function with a special ABI).
>
OK, but this seems like quite a lot of work for what is after all just an
optimization - eliminating one call to the relocating constructor, that
could have been elided anyway?
And as sugar, we could add that if the prvalue-assignment operator is
> defaulted in its declaration then the reloc aliasing is implicit. This
> works as today there is no default code generation for the
> prvalue-assignment operator.
>
> // in class declaration:
> T& operator=(T) = default: // reloc is implicitly added
>
Sure, that's fine since it gets inlined anyway; there is no symbol to have
ABI.
That being said, now lies the problem on how to write a custom
> implementation for this assignment operator. For the relocation ctor, we
> worked around this by saying that omitted subobject initializers would
> automatically call the appropriate reloc ctor of said subobject. We cannot
> do that magic for the reloc-assign, as there is no initializer list.
> However, I believe that std::relocate can come to the rescue here:
>
> class T : B
> {
> D _d;
> reloc T& operator=(T rhs)
> { // exception safety is omitted for the example
> B::operator=(std::relocate(static_cast<B*>(&rhs)));
> _d = std::relocate(&rhs._d);
> return *this;
> }
> };
>
> std::relocate is a nice fit here as, thanks to the aliasing, we know that
> rhs dtor will not be called. In the same manner, std::relocate could also
> be used to initialize subobjects in the reloc ctor.
>
Sorry, no. Aliasing doesn't prevent the destructor being called, it just
means that the parameter is the same object as (has the same address as)
the argument. It still has to be (will be) destroyed, either by the
destructor or by the relocating constructor. If you want to write code
like that, you need to relocate rhs into a buffer (placement-new-relocate)
or union.
As a side note, I wonder to what extent std::relocate could not be a
> special function that, as reloc, simply turns some address into a prvalue,
> without actually performing any relocation. That could allow nice
> optimization bits in user-defined reloc ctor or assignment operator.
>
Definitely, if that can be made to work. That's really nice; it supports my
idea of relocation being conceptually a change in value category.
Sure, but the user is capable of doing this themselves?
>>
>> 1. T& operator=(T rhs) { static_assert(noexcept(...)); this->~T(); return
>> *new (this) T(reloc rhs); }
>> 2. T& operator=(T) = default; // ill-formed if ~T() is user-provided
>>
>> Yes, they don't get automatic selection between the two, but I don't
>> really see that as an issue; it'd be quite surprising.
>>
>
> I feel mixed with having = default; doing only memberwise assignments:
>
> - This will force all users that have a user-provided dtor to write
> their own reloc-assignment operator, while it is likely that their reloc
> ctor and dtor are noexcept :/
> - If = default also generates destroy-and-relocate then it enables
> nice recursion. Without this some classes will need to write their own
> reloc-assignment operator while the default generated destroy-and-relocate
> would fit like a glove. Take unique_ptr for instance. This one has a
> legitimate user-provided dtor, and as such is not eligible to
> memberwise-reloc-assign (which is good, as it would otherwise leak its own
> resource). As a consequence std::unique_ptr would need to provide a
> reloc-assign definition, which will ultimately, just free its resource and
> steal that of the source object. Something that the default generated
> destroy-and-relocate would do for free.
>
> My point is that, if = default (a) generates destroy-and-relocate, and (b)
> falls back to memberwise-reloc-assignments, then users will almost never
> need to write any implementation. Think of case (a) as the base/halting
> cases of any recursive function.
>
Against this, there's the inconsistency with the other special assignment
operators (copy and move). Consistency is important for teachability and
to ensure that users have the correct mental model. Unless you're
proposing to change those as well...
I feel that it's OK to expect library authors (a small proportion of users
in general) to write `T& operator=(T rhs) noexcept { this->~T(); return
*new (this) T(reloc rhs); }`. It's only 30 tokens, against 10 to explicitly
default the relocating assignment operator.
Received on 2022-08-19 12:29:30