C++ Logo

std-proposals

Advanced search

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

From: Edward Catmur <ecatmur_at_[hidden]>
Date: Wed, 2 Mar 2022 22:57:16 +0100
On Wed, 2 Mar 2022 at 12:33, Maciej Cencora via Std-Proposals <
std-proposals_at_[hidden]> wrote:

> Lol. I am not a proponent of Rust, I have never written a single line
> of code in that language, nor did I read any book about it.
>
> If you think I am wrong, then please show me how this mentioned issue
> can be solved without a language level construct?
>

It can (and should) be solved in the library, as we solve many problems in
the library for which other languages would need a language level construct.

The problem with a `relocate` operator per se is that in real-world usage
it can be arbitrarily complicated to determine whether a variable has been
relocated from and that following uses should be rejected; we can't use
linear/affine types or Rust's lifetime analysis. Instead, we should solve
this in the library: use `std::optional<T>` and give it a (hypothetical;
bikeshed) `T std::optional<T>::pop()` method that returns a prvalue
relocated from the contained object (or moved/copied followed by destroy,
for non-relocatable types), and sets the optional to disengaged. Then,
identifying programs where the optional is used after it has been
disengaged is a matter of QOI, and can be solved by static analysis and
testing with sanitizers or wide-contract methods. And in many cases the
engaged flag of the optional would be optimized out of the program binary
(again, we are historically and presently happy to rely on the optimizer).

For efficiency (trivial std::unique_ptr, std::list with allocating default
ctor) and for never-empty types (gsl::not_null) we will need a way to write
or (preferably) default the relocation operation on class types; this could
be accomplished via (suitably) qualified (or perhaps specified) member
functions, writing the relocation operation as a qualified conversion
function to the same (class) type and allowing other other destructive
operations on prvalues (e.g. std::unique_ptr::release) to obviate calling
the destructor. Coming up with a syntax (other than `= default`, on the
relocation operation itself) that is guaranteed leak-free is a bit tricky,
but I'm working on it (from time to time).

The relocation operation would automatically be called on return of an
id-expression or similar (considered equivalent to and preferred over
move-and-destroy), but there would also need to be a way for library code
(such as `optional`) to invoke the relocate operation on objects whose
lifetime it manages, where currently it might call the move constructor
followed by the destructor. A (magic) library function `T
relocate_or_move_and_destroy_at(T*)` would be sufficient for (Standard and
third-party) library authors, while sufficiently off-putting to end users
that they would be steered to use `std::optional`; the overconfident would
be free to use `alignas(T) std::byte buf[sizeof(T)]`.

The library should similarly offer a type trait `is_trivially_relocatable`
(*not* `is_relocatable`, since there would be no way for user code to
invoke the relocation operation directly without fallback to
move-and-destroy), allowing (user and library) code to apply all 3 of the
memcpy/memmove optimizations mentioned by Arthur; `any` and `swap` would
use the trait directly, and `vector` via
`uninitialized_relocate_or_move_and_destroy_n`. Clearly, scalars (and
arrays thereof, etc) would be trivially relocatable, as would aggregates
with all trivially relocatable members; (other) user-defined special member
functions would disable the relocation operation on class types, but it
could be reenabled (trivially, if appropriate) with `= default` syntax.

Finally, note that there is no need for a relocating assignment operator;
any class for which it would be trivial will be itself trivial (assignment
can't be trivial for `std::unique_ptr` because any existing resource has to
be deleted); std::list doesn't care since both sides already have their
sentinel node allocated; and gsl::not_null can write its assignment
operator to take its argument by value and swap (the issue of delayed
destruction of by-value arguments being sufficiently abstruse not to be
worth worrying about). Considering Arthur's criteria, `any` and `swap` are
already covered by trivial relocatability, and `vector` can't benefit since
destructors would need to be called on either the source or target range
anyway.

Received on 2022-03-02 21:57:28