Date: Wed, 1 Jun 2022 18:01:00 -0600
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.
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 00:01:13