Date: Tue, 3 May 2022 11:42:37 +0200
Hello all,
I've been digesting yesterday discussions, and here is what I can propose
to move forward:
- types cannot be relocated if they don't have a copy constructor or a
move constructor defined and accessible.
- we remove relocation references and the relocation constructor.
- we introduce a new "operator reloc(T&& rhs)" static member function in
classes, which will act both as a constructor and a destructor (much like
P0023 relocator). This function can be defaulted, implicitly declared and
defined just like constructors. This would likely require new syntax bits,
more on that later.
- we keep the reloc operator. Calling reloc on an object will "relocate"
the instance and mark the end of scope of its name. How the object is
"relocated" is left to compiler vendors. The language offers the following
relocation methods:
- If the type is trivially relocatable (p1144), then using
std::memcpy is authorized. If this method is used then the language
enforces that the destructor of the relocated object is not called.
- If the type provides an accessible "operator reloc" method, then
it can be used to perform the relocation. Again the language
enforces that
the destructor of the relocated object is not called.
- Simply use a regular move (std::move). The destructor of the
moved instance is called normally (i.e. when it goes out of scope).
- reloc still discards any cv-qualifiers. This means that relocating
const variables is possible with reloc. This is true even if the "regular
move" method is picked. For instance `const T a; foo(reloc a);` could
yield `const T a; foo(T{const_cast<T&&>(a)});`.
This offers several benefits:
- no longer need a new type of value (relocation references).
- objects passed to "operator reloc" (rebranded relocation ctor) no
longer need to be destructed (and must not be).
- pure relocation (memcpy or "operator reloc" member function) is opt-in
by compilers, so we no longer force an ABI break. Besides there are still
cases where pure relocation can happen without breaking the ABI.
- reloc operator keeps its benefits: leads to safer code by ensuring
that relocated objects are not reused (dangling references left aside), and
allows for better const correctness in programs as const objects can be
relocated.
- "operator reloc" will most of the time be defaulted or not needed
(because of trivially relocatable). It should only need to be defined when
some self references need to be adjusted.
- library vendors can still perform nice optimizations in their
container implementations, by using either trivial relocation (p1144),
"operator reloc" member function, or move ctor as last resort.
This is not however exactly the proposal I first had in mind. What first
motivated me to write this was to allow for classes to be relocatable-only
(non-movable and non-copyable), which is only partially achieved here. This
is hard to conciliate with (a) the move status quo and (b) the ABI break it
would incur to forcibly alleviate the destructor call on relocated
objects...
Anyway I am also convinced of the benefits of the reloc operator and that
is why I am willing to pursue this.
*About the "operator reloc" member function:*
I propose this function to have this signature "operator reloc(T&&)
[noexcept]". noexcept shall be optional but throwing from such a function
shall be an UB (just like it is an UB to throw from a destructor IIRC).
This function shall return a new instance, just like a constructor. In fact
it shall share many similarities with constructors:
- offer a base class and data-member initialisation syntax
- can be implicitly declared and defined
- can be defaulted
This function shall also act as a destructor with regards to its only
parameter (the relocated instance). It shall be an UB to call the
destructor on an object after it was passed as a parameter to its "operator
reloc".
I suggest the following initializer syntax for operator reloc:
struct T : B {
M data;
// user-implementation equivalent to =default;
operator reloc(T&& rhs) noexcept : B reloc{std::move(rhs)}, data
reloc{std::move(rhs.data)} {}
};
@Edward Catmur: I read your suggestion about having automatic member
initialisation as if defaulted, but I think it would contrast too much from
constructors. Besides, users may have specific initialisation needs? We
could still come up with something like 'operator reloc(T&& rhs) noexcept =
bitcopies' (P1029) and still provide a function body to adjust self
references...
This is not perfect as developers will now be tempted to explicitly call
this constructor which may lead to disasters:
std::string a;
std::string b reloc{std::move(a)}; // disaster will happen
// instead of: std::string b{reloc a}; (which will prevent the
destructor call of a).
Although relocation references had many flaws, it had the advantage of
requiring the use of const_cast which would deter many developers from
using them directly (we all know const_cast is dangerous).
Any ideas on that part (and in general :p) are welcome.
thank you & best regards,
Sébastien
I've been digesting yesterday discussions, and here is what I can propose
to move forward:
- types cannot be relocated if they don't have a copy constructor or a
move constructor defined and accessible.
- we remove relocation references and the relocation constructor.
- we introduce a new "operator reloc(T&& rhs)" static member function in
classes, which will act both as a constructor and a destructor (much like
P0023 relocator). This function can be defaulted, implicitly declared and
defined just like constructors. This would likely require new syntax bits,
more on that later.
- we keep the reloc operator. Calling reloc on an object will "relocate"
the instance and mark the end of scope of its name. How the object is
"relocated" is left to compiler vendors. The language offers the following
relocation methods:
- If the type is trivially relocatable (p1144), then using
std::memcpy is authorized. If this method is used then the language
enforces that the destructor of the relocated object is not called.
- If the type provides an accessible "operator reloc" method, then
it can be used to perform the relocation. Again the language
enforces that
the destructor of the relocated object is not called.
- Simply use a regular move (std::move). The destructor of the
moved instance is called normally (i.e. when it goes out of scope).
- reloc still discards any cv-qualifiers. This means that relocating
const variables is possible with reloc. This is true even if the "regular
move" method is picked. For instance `const T a; foo(reloc a);` could
yield `const T a; foo(T{const_cast<T&&>(a)});`.
This offers several benefits:
- no longer need a new type of value (relocation references).
- objects passed to "operator reloc" (rebranded relocation ctor) no
longer need to be destructed (and must not be).
- pure relocation (memcpy or "operator reloc" member function) is opt-in
by compilers, so we no longer force an ABI break. Besides there are still
cases where pure relocation can happen without breaking the ABI.
- reloc operator keeps its benefits: leads to safer code by ensuring
that relocated objects are not reused (dangling references left aside), and
allows for better const correctness in programs as const objects can be
relocated.
- "operator reloc" will most of the time be defaulted or not needed
(because of trivially relocatable). It should only need to be defined when
some self references need to be adjusted.
- library vendors can still perform nice optimizations in their
container implementations, by using either trivial relocation (p1144),
"operator reloc" member function, or move ctor as last resort.
This is not however exactly the proposal I first had in mind. What first
motivated me to write this was to allow for classes to be relocatable-only
(non-movable and non-copyable), which is only partially achieved here. This
is hard to conciliate with (a) the move status quo and (b) the ABI break it
would incur to forcibly alleviate the destructor call on relocated
objects...
Anyway I am also convinced of the benefits of the reloc operator and that
is why I am willing to pursue this.
*About the "operator reloc" member function:*
I propose this function to have this signature "operator reloc(T&&)
[noexcept]". noexcept shall be optional but throwing from such a function
shall be an UB (just like it is an UB to throw from a destructor IIRC).
This function shall return a new instance, just like a constructor. In fact
it shall share many similarities with constructors:
- offer a base class and data-member initialisation syntax
- can be implicitly declared and defined
- can be defaulted
This function shall also act as a destructor with regards to its only
parameter (the relocated instance). It shall be an UB to call the
destructor on an object after it was passed as a parameter to its "operator
reloc".
I suggest the following initializer syntax for operator reloc:
struct T : B {
M data;
// user-implementation equivalent to =default;
operator reloc(T&& rhs) noexcept : B reloc{std::move(rhs)}, data
reloc{std::move(rhs.data)} {}
};
@Edward Catmur: I read your suggestion about having automatic member
initialisation as if defaulted, but I think it would contrast too much from
constructors. Besides, users may have specific initialisation needs? We
could still come up with something like 'operator reloc(T&& rhs) noexcept =
bitcopies' (P1029) and still provide a function body to adjust self
references...
This is not perfect as developers will now be tempted to explicitly call
this constructor which may lead to disasters:
std::string a;
std::string b reloc{std::move(a)}; // disaster will happen
// instead of: std::string b{reloc a}; (which will prevent the
destructor call of a).
Although relocation references had many flaws, it had the advantage of
requiring the use of const_cast which would deter many developers from
using them directly (we all know const_cast is dangerous).
Any ideas on that part (and in general :p) are welcome.
thank you & best regards,
Sébastien
Received on 2022-05-03 09:42:48