Date: Thu, 2 Jun 2022 10:39:38 +0200
Hi Edward,
Thank you for those clarifications.
I have a few other points to clarify though '^^
*About T(T) vs operator reloc(T&&):*
T(T src) is special in two ways:
- It alleviates the destructor call on src;
- Behind the scenes, T(T) only gets the address of the source object (as
if by T(T&)), and not an actual copy as its signature would suggest.
Right?
In addition, in T(T src), src becomes an lvalue in that context, and as
such writing manually the default implementation becomes impossible:
struc T : B
{
D _data;
T(T src) : B{src}, _data{src._data} {} // /!\ Calls copy constructors /!\
};
Which means that we will have to rely on the same tricks as operator
reloc(T&&) (i.e. non-written initializers will be initialized by the reloc
ctor or synthesized relocation).
My point is that I don't see much benefit of T(T) over operator reloc(T&&).
- Semantically speaking, I get your point that T(T) would be more
correct, but it seems so counter-intuitive to use as it does not obey the
same rules as any other function that takes a prvalue as parameter.
- On the other hand operator reloc(T&&) doesn't lie on the fact that it
takes an address, and its signature doesn't imply that its source parameter
will be destructed. Instead, the very name of the operator suggests the
opposite.
We can still have `reloc obj` to merely change the value category of `obj`
instead of returning the new instance, regardless of the signature we
choose for relocation (T(T), operator reloc(T&&) or even operator reloc(T)).
*About reloc operator changing the value category:*
Consider the following code:
void prsink(T);
void xsink(T&&);
T x, y;
prsink(reloc x); // here destructor call on x is avoided
xsink(reloc y); // destructor of y is called
Before, the reloc operator was in charge of making the decision of not
calling some destructor. This is no longer the case, as it now really
depends on how the prvalue is consumed. Am I right?
void sink(T);
void sink(T&&);
void foo(T val)
{
sink(reloc val);
}
What happens now if T has opted-out of the ABI break with the
non-standardized attribute? Before the reloc operator was in charge of
building the new instance and could fallback to the move constructor. Now
this is no longer the case as it depends on how the prvalue is consumed.
Isn't it problematic as now the language needs to distinguish between
prvalues whose destructor calls can be alleviated and the others? Which
overload of sink is picked in that case?
Best regards,
Sébastien
On Thu, Jun 2, 2022 at 2:01 AM Edward Catmur <ecatmur_at_[hidden]> wrote:
> On Wed, 1 Jun 2022 at 09:53, Sébastien Bini <sebastien.bini_at_[hidden]>
> wrote:
>
>> The user can write destroy+relocate if they want; the language shouldn't
>>> be doing that.
>>>
>>
>> As a user I would feel uncomfortable explicitly calling the destructor on
>> *this to reconstruct it again.
>>
>
> Well, quite. And destroy-and-recreate is a legitimate implementation of
> copy- and move-assignment operators (see e.g. rapidjson::Document
> <https://github.com/Tencent/rapidjson/blob/232389d4f1012dddec4ef84861face2d2ba85709/include/rapidjson/document.h#L921>)
> but I would always advise to use copy-and-swap idiom.
>
> This does require a bit of a conceptual leap, yes. The key is to
>>> understand that operator reloc *changes the value category* of a named
>>> object, from lvalue to prvalue. This is because relocation is the inverse
>>> operation to prvalue materialization. So by the time the relocating
>>> constructor is invoked, the argument is already a prvalue (just, uh, a
>>> prvalue that occupies storage).
>>>
>>
>> Just to be sure I understand correctly: the reloc operator would no
>> longer return a newly built instance (i.e. a temporary), but instead would
>> merely transform the value category from lvalue to prvalue?
>> And then whether a temporary is materialized or not depends on the
>> context, right?
>>
>
> Yes, correct. One way to think of it is as if the preceding scope were
> transformed into an anonymous function returning that value.
>
> Consider a type T with that putative relocation constructor T(T):
>>
>> Case 1:
>> void foo(T a_foo);
>>
>> T var;
>> foo(reloc var);
>>
>> In `foo(reloc var);` is a_foo constructed using T(T)?
>>
>> If so then `var`'s destruction is delegated to T(T), which prevents the
>> destructor call of its parameter, right?
>>
>
> Yes, correct. Except that the call to T(T) may be elided, in which case
> `var` is constructed directly in the parameter `a_foo` of `foo`.
>
> Case 2:
>> Now let's add a T& operator=(T):
>>
>> T objA, objB;
>> objA = reloc objB;
>>
>> Following the same logic as above, objB is passed to relocation
>> constructor, and then to objA's assignment operator, right?
>>
>
> Yes, modulo elision.
>
>
>> Case 3:
>> T obj;
>> reloc obj; // premature end-of-scope
>>
>> This turns obj into a prvalue that is captured nowhere. I guess in this
>> case the destructor of obj is simply called?
>>
>
> Yes.
>
>
>> Case 4:
>> void bar(T&& bar_param);
>>
>> T var;
>> bar(reloc var);
>>
>> Again, reloc var turns var into a prvalue, but bar cannot capture it. So
>> a temporary object of type T is created and its address is passed to bar,
>> right?
>>
>
> The rvalue reference binds directly to the prvalue; there is no temporary
> object created. So &var == &bar_param.
>
> If I understand correctly, then this could simplify some aspects of how
>> operator reloc works.
>>
>> However I fear many users will be lost (most C++ programmers that I know
>> have no knowledge of value categories), and will find it quite disturbing
>> to have a relocation constructor with that signature T(T). Sure operator
>> reloc(T&&) may be confusing as well, but my personal take is that it would
>> be less so :/
>>
>
> That's an opportunity for educators, then. Really, this simplifies the
> value category model: copy constructors (prefer to) construct from lvalues,
> move constructors from xvalues, and relocation constructors from prvalues.
> So that's the model to use.
>
> There is another reason to model relocation as a by-value constructor: it
> enables delegating relocation to another constructor i.e. T(T src) :
> T(reloc src, ...) {}. That said, the delegated-to constructor would not
> benefit from the default memberwise relocation that the relocating
> constructor does as a special member function, and also it would not
> benefit from the destructor being obviated, so this would be extreme expert
> level only. But it's still worth enabling.
>
Thank you for those clarifications.
I have a few other points to clarify though '^^
*About T(T) vs operator reloc(T&&):*
T(T src) is special in two ways:
- It alleviates the destructor call on src;
- Behind the scenes, T(T) only gets the address of the source object (as
if by T(T&)), and not an actual copy as its signature would suggest.
Right?
In addition, in T(T src), src becomes an lvalue in that context, and as
such writing manually the default implementation becomes impossible:
struc T : B
{
D _data;
T(T src) : B{src}, _data{src._data} {} // /!\ Calls copy constructors /!\
};
Which means that we will have to rely on the same tricks as operator
reloc(T&&) (i.e. non-written initializers will be initialized by the reloc
ctor or synthesized relocation).
My point is that I don't see much benefit of T(T) over operator reloc(T&&).
- Semantically speaking, I get your point that T(T) would be more
correct, but it seems so counter-intuitive to use as it does not obey the
same rules as any other function that takes a prvalue as parameter.
- On the other hand operator reloc(T&&) doesn't lie on the fact that it
takes an address, and its signature doesn't imply that its source parameter
will be destructed. Instead, the very name of the operator suggests the
opposite.
We can still have `reloc obj` to merely change the value category of `obj`
instead of returning the new instance, regardless of the signature we
choose for relocation (T(T), operator reloc(T&&) or even operator reloc(T)).
*About reloc operator changing the value category:*
Consider the following code:
void prsink(T);
void xsink(T&&);
T x, y;
prsink(reloc x); // here destructor call on x is avoided
xsink(reloc y); // destructor of y is called
Before, the reloc operator was in charge of making the decision of not
calling some destructor. This is no longer the case, as it now really
depends on how the prvalue is consumed. Am I right?
void sink(T);
void sink(T&&);
void foo(T val)
{
sink(reloc val);
}
What happens now if T has opted-out of the ABI break with the
non-standardized attribute? Before the reloc operator was in charge of
building the new instance and could fallback to the move constructor. Now
this is no longer the case as it depends on how the prvalue is consumed.
Isn't it problematic as now the language needs to distinguish between
prvalues whose destructor calls can be alleviated and the others? Which
overload of sink is picked in that case?
Best regards,
Sébastien
On Thu, Jun 2, 2022 at 2:01 AM Edward Catmur <ecatmur_at_[hidden]> wrote:
> On Wed, 1 Jun 2022 at 09:53, Sébastien Bini <sebastien.bini_at_[hidden]>
> wrote:
>
>> The user can write destroy+relocate if they want; the language shouldn't
>>> be doing that.
>>>
>>
>> As a user I would feel uncomfortable explicitly calling the destructor on
>> *this to reconstruct it again.
>>
>
> Well, quite. And destroy-and-recreate is a legitimate implementation of
> copy- and move-assignment operators (see e.g. rapidjson::Document
> <https://github.com/Tencent/rapidjson/blob/232389d4f1012dddec4ef84861face2d2ba85709/include/rapidjson/document.h#L921>)
> but I would always advise to use copy-and-swap idiom.
>
> This does require a bit of a conceptual leap, yes. The key is to
>>> understand that operator reloc *changes the value category* of a named
>>> object, from lvalue to prvalue. This is because relocation is the inverse
>>> operation to prvalue materialization. So by the time the relocating
>>> constructor is invoked, the argument is already a prvalue (just, uh, a
>>> prvalue that occupies storage).
>>>
>>
>> Just to be sure I understand correctly: the reloc operator would no
>> longer return a newly built instance (i.e. a temporary), but instead would
>> merely transform the value category from lvalue to prvalue?
>> And then whether a temporary is materialized or not depends on the
>> context, right?
>>
>
> Yes, correct. One way to think of it is as if the preceding scope were
> transformed into an anonymous function returning that value.
>
> Consider a type T with that putative relocation constructor T(T):
>>
>> Case 1:
>> void foo(T a_foo);
>>
>> T var;
>> foo(reloc var);
>>
>> In `foo(reloc var);` is a_foo constructed using T(T)?
>>
>> If so then `var`'s destruction is delegated to T(T), which prevents the
>> destructor call of its parameter, right?
>>
>
> Yes, correct. Except that the call to T(T) may be elided, in which case
> `var` is constructed directly in the parameter `a_foo` of `foo`.
>
> Case 2:
>> Now let's add a T& operator=(T):
>>
>> T objA, objB;
>> objA = reloc objB;
>>
>> Following the same logic as above, objB is passed to relocation
>> constructor, and then to objA's assignment operator, right?
>>
>
> Yes, modulo elision.
>
>
>> Case 3:
>> T obj;
>> reloc obj; // premature end-of-scope
>>
>> This turns obj into a prvalue that is captured nowhere. I guess in this
>> case the destructor of obj is simply called?
>>
>
> Yes.
>
>
>> Case 4:
>> void bar(T&& bar_param);
>>
>> T var;
>> bar(reloc var);
>>
>> Again, reloc var turns var into a prvalue, but bar cannot capture it. So
>> a temporary object of type T is created and its address is passed to bar,
>> right?
>>
>
> The rvalue reference binds directly to the prvalue; there is no temporary
> object created. So &var == &bar_param.
>
> If I understand correctly, then this could simplify some aspects of how
>> operator reloc works.
>>
>> However I fear many users will be lost (most C++ programmers that I know
>> have no knowledge of value categories), and will find it quite disturbing
>> to have a relocation constructor with that signature T(T). Sure operator
>> reloc(T&&) may be confusing as well, but my personal take is that it would
>> be less so :/
>>
>
> That's an opportunity for educators, then. Really, this simplifies the
> value category model: copy constructors (prefer to) construct from lvalues,
> move constructors from xvalues, and relocation constructors from prvalues.
> So that's the model to use.
>
> There is another reason to model relocation as a by-value constructor: it
> enables delegating relocation to another constructor i.e. T(T src) :
> T(reloc src, ...) {}. That said, the delegated-to constructor would not
> benefit from the default memberwise relocation that the relocating
> constructor does as a special member function, and also it would not
> benefit from the destructor being obviated, so this would be extreme expert
> level only. But it's still worth enabling.
>
Received on 2022-06-02 08:39:50