C++ Logo

std-proposals

Advanced search

Re: [std-proposals] Relocation in C++

From: Sébastien Bini <sebastien.bini_at_[hidden]>
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

Received on 2022-05-03 09:42:48