C++ Logo


Advanced search

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

From: Edward Catmur <ecatmur_at_[hidden]>
Date: Tue, 31 May 2022 10:10:52 -0600
On Tue, 31 May 2022 at 02:51, Sébastien Bini <sebastien.bini_at_[hidden]>

> Actually I am not sure there are any real benefits of having `T
> std::relocate(T*)` if we have the reloc operator (which is a safer version
> of std::relocate) and std::relocate_at. swap can be written without
> std::relocate:
> template<class T> requires is_noexcept_relocatable_v<T>
> void swap(T& lhs, T& rhs)
> {
> std::aligned_storage_t<sizeof(T), alignof(T)> tmp;
> std::relocate_at(&lhs, reinterpret_cast<T*>(&tmp));
> std::relocate_at(&rhs, &lhs);
> std::relocate_at(reinterpret_cast<T*>(&tmp), &rhs);
> }

Nit: std::aligned_storage is deprecated, we just use alignas(T) std::byte

More importantly, I don't think this would be constexpr at present, because
of the reinterpret_cast. Either relocate_at should have overloads taking
void* with T specified explicitly, or this will need to be written using

template<class T> requires is_noexcept_relocatable_v<T>
constexpr void swap(T& lhs, T& rhs) {
    union { char c; T t; } u = { .c = '\0' };
    relocate_at(&lhs, &u.t);
    relocate_at(&rhs, &lhs);
    relocate_at(&u.t, &rhs);
    u.c = '\0';

And I'm not 100% sure that this is constexpr (&u.t is doing a lot of work).
So I'd prefer the version using std::relocate, std::relocate_at, operator
reloc. This is a good demonstration of why all 3 of std::relocate,
std::relocate_at, operator reloc are necessary.

On Tue, 31 May 2022 at 07:31, Arthur O'Dwyer <arthur.j.odwyer_at_[hidden]>

> > gsl::non_null<int*> p1, p2, p3 = ...;
>> > gsl::non_null<int*> q1 = reloc p1; // OK in your world
>> > q1 = reloc p2; // OK??
>> > std::swap(q1, p3); // OK??
>> `reloc obj` (with obj of type T) merely returns a temporary object of
>> type T, built by relocating from obj (just like std::relocate). obj is
>> considered destructed and as such is not left in a moved-from state. We
>> rely on mechanisms such as copy-elision or clearly identified patterns
>> (object initialisation with a reloc statement, like: `auto a = reloc obj;`)
>> to remove that temporary in most cases.
>> With `q1 = reloc p2;`, as it is phrased in the paper, reloc p2 will
>> return a temporary that is assigned into q1. p2 is considered destructed
>> (relocated into the temporary). Then q1's move assignment operator will be
>> called.
> q1 has no "move assignment operator", remember? We're postulating that
> gsl::non_null<int*> is one of these types that lacks a moved-from state. So
> it certainly cannot have a move assignment operator nor a move constructor.

Oops, my bad. The key insight is that gsl::non_null can have a *by-value*
assignment operator:

non_null& operator=(non_null rhs) {
    (reloc rhs).swap(*this);
    return *this;

>> > Similarly, if you "relocated" a `std::mutex`, *bad things would happen*.
>> You can't have a mutex suddenly change memory addresses and expect the rest
>> of the program to just be cool with that.
>> Yes there are objects that are not even relocatable, like std::mutex.
>> Such types can prohibit relocation by deleting their operator reloc:
>> `operator reloc(T&&) = delete;` (which should already be implicitly deleted
>> since their move constructor is also deleted.)
> Hm. Could you write out the rules that you're imagining for
> operator reloc(T&&) = default;
> operator reloc(T&&) = delete;
> as well as what happens by default?
> (IIUC, you're imagining that `operator reloc` would work the same as
> `operator<=>` — an explicitly =defaulted definition gets a memberwise
> implementation, but if you don't say anything then the operator is absent,
> not defaulted.)
> It sounds like you're imagining simultaneously that
> struct S1 {
> M m;
> S1(S1&&);
> operator reloc(S1&&) = default;
> };
> will have a defaulted (memberwise) `operator reloc`, and that
> struct S2 {
> M m;
> S2(S2&&) = delete;
> };
> will have a deleted `operator reloc` "since their move constructor is also
> deleted" (i.e., S2's operator reloc won't just be absent; it'll be deleted).

My take:

* operator reloc(T&&) = default:
Each direct base and member is relocated, in declaration order. Relocation
is by invoking operator reloc if present, otherwise by move-and-destroy
(the "relocation operation"); destructors (for move-and-destroy subobjects)
are not interleaved but are invoked at the end, in reverse declaration
order. Trivial if each direct base and member is trivially relocatable;
that is, if the selected relocation operation is trivial. Deleted if the
relocation operation for any direct base or member is inaccessible or
deleted, or if the class has declared destructor (not declared as defaulted
or deleted) and any direct base or member has noexcept(false) relocation
operation. noexcept(true) if each direct base and member is nothrow
relocatable, otherwise noexcept(false), in which case cleanup destroys
subobjects of the source object in reverse declaration order (whose
lifetime has not already ended), followed by subobjects of the destination
object in reverse declaration order (whose lifetime has begun); the class
destructor is not called.

* operator reloc(T&&) = delete;
The class is not relocatable: keyword reloc, std::relocate etc. will fail;
type_traits will report false; and a deriving or composing class will have
deleted operator reloc, even if the class has accessible non-deleted move
constructor and destructor. (In that case, it can be made (non-trivially)
relocatable by wrapping in a class whose operator reloc explicitly invokes
move-and destroy, i.e.: struct U : T { operator reloc(U&& rhs) :
T(std::move(rhs)) {} };.)

* If not declared:
Rule of Zero: if no Ro5 SMF (dtor, copy/move ctor, copy/move assign) is
declared (including declared as defaulted or deleted), then implicitly
present as if declared as defaulted. Otherwise, absent; the class is still
relocatable (possibly trivially relocatable) if move ctor and dtor are
accessible and not deleted.

Received on 2022-05-31 16:11:05