P1144R6
Object relocation in terms of move plus destroy

Published Proposal,

Issue Tracking:
Inline In Spec
Author:
Audience:
LEWG, EWG
Project:
ISO/IEC JTC1/SC22/WG21 14882: Programming Language — C++
Draft Revision:
30
Current Source:
github.com/Quuxplusone/draft/blob/gh-pages/d1144-object-relocation.bs
Current:
rawgit.com/Quuxplusone/draft/gh-pages/d1144-object-relocation.html

Abstract

We define a new verb, "relocate," which is equivalent to a move and a destroy. For many C++ types, the "relocate" operation is implementable as a single memcpy. We provide a standard trait to detect types which are trivially relocatable, for the benefit of library writers. We provide a rule that correctly propagates trivial relocatability among Rule-of-Zero types. Finally, we provide a portable way for a user-defined type (e.g. boost::shared_ptr) to warrant that it is trivially relocatable. This paper was presented at C++Now 2019 as a 90-minute talk titled "Trivially Relocatable."

1. Changelog

2. Introduction and motivation

C++17 knows the verbs "move," "copy," "destroy," and "swap," where "swap" is a higher-level operation composed of several lower-level operations. To this list we add the verb "relocate," which is a higher-level operation composed of exactly two lower-level operations. Given an object type T and memory addresses src and dst, the phrase "relocate a T from src to dst" means no more or less than "move-construct dst from src, and then immediately destroy the object at src."

Any type which is both move-constructible and destructible is relocatable. The notion can be modified by adverbs: we say that a type is nothrow relocatable if its relocation operation is noexcept, and we say that a type is trivially relocatable if its relocation operation is trivial (which, just like trivial move-construction and trivial copy-construction, means "tantamount to memcpy").

In practice, almost all relocatable types are trivially relocatable: std::unique_ptr<int>, std::vector<int>, std::string. Non-trivially relocatable types exist but are rare; one example is std::list<int>. See Appendix C: Examples of non-trivially relocatable class types.

P1144 provides a way for the programmer to warrant to the compiler that a resource-managing type is trivially relocatable, and provides an algorithm by which the compiler can recursively infer that a Rule-of-Zero type is trivially relocatable.

The most important thing about P1144 relocation is that it is backward compatible and does not break either API or ABI. My intention is simply to legalize the well-understood tricks that every industry codebase is already doing in practice (see [BSL], [EASTL], [Folly]). P1144 is not intended to change the behavior of any existing source code (except to speed it up). P1144 is not intended to require any major work from standard library vendors.

As observed in [CppChat] (@21:57): Just as with C++11 move semantics, you can write benchmarks to show whatever speedup you like. The more complicated your types' move-constructors and destructors, the more time you save by eliminating calls to them.

2.1. Optimizations enabled by trivial relocatability

2.1.1. Vector resize

If we have a reliable way of detecting "trivial relocatability," we can optimize any routine that performs the moral equivalent of realloc, including

std::vector<R>::resize
std::vector<R>::reserve
std::vector<R>::emplace_back
std::vector<R>::push_back

[Bench] (presented at C++Now 2018) shows a 3x speedup on std::vector<std::unique_ptr<int>>::resize. This Reddit thread demonstrates a similar 3x speedup using the online tool Quick-Bench.

2.1.2. Swap

Given a reliable way of detecting trivial relocatability, we can optimize any routine that uses the moral equivalent of std::swap, such as

std::swap
std::vector<R>::insert

Optimizing std::swap produces massive code-size improvements for all swap-based algorithms, including std::sort and std::rotate. See @19:56–21:22 in my C++Now 2019 talk, and see this Godbolt.

But see § 6.6 Confusing interactions with std::pmr and vector::insert for further discussion of vector::insert.

2.1.3. More efficient small-buffer type-erasure

Given a reliable way of detecting trivial relocatability, we can de-duplicate the code generated by small-buffer-optimized (SBO) type-erasing wrappers such as std::function and std::any. For these types, a move of the wrapper object is implemented in terms of a relocation of the contained object. (See for example libc++'s std::any, where the function that performs the relocation operation is confusingly named __move.) In general, the relocate operation for a contained type C must be uniquely codegenned for each different C, leading to code bloat. But a single instantiation suffices to relocate every trivially relocatable C in the program. A smaller number of instantiations means faster compile times, a smaller text section, and "hotter" code (because a relatively higher proportion of your code now fits in icache).

2.1.4. More efficient fixed-capacity containers

Given a reliable way of detecting trivial relocatability, we can optimize the move-constructor of fixed_capacity_vector<R,N>, which can be implemented naïvely as an element-by-element move (leaving the source vector’s elements in their moved-from state), or can be implemented efficiently as an element-by-element relocate (leaving the source vector empty).

For a detailed analysis of this case, see [FixedCapacityVector].

Note: boost::container::static_vector<R,N> currently implements the naïve element-by-element-move strategy.

2.1.5. Assertions, not assumptions

Some concurrent data structures might reasonably assert the trivial relocatability of their elements, just as they sometimes assert the stronger property of trivial copyability today.

2.2. The most important benefit

Many real-world codebases already contain templates which require trivial relocatability of their template parameters, but currently have no way to verify trivial relocatability. For example, [Folly] requires the programmer to warrant the trivial relocatability of any type stored in a folly::fbvector:

class Widget {
    std::vector<int> lst_;
};

folly::fbvector<Widget> vec;  // FAILS AT COMPILE TIME for lack of warrant

But this merely encourages the programmer to add the warrant and continue. An incorrect warrant will be discovered only at runtime, via undefined behavior. See Allocated memory contains pointer to self, [FollyIssue889], and (most importantly) @27:26–31:47 in my C++Now 2019 talk.

class Gadget {
    std::list<int> lst_;
};
// sigh, add the warrant on autopilot
template<> struct folly::IsRelocatable<Gadget> : std::true_type {};

folly::fbvector<Gadget> vec;  // CRASHES AT RUNTIME due to fraudulent warrant

If this proposal is adopted, then Folly can start using static_assert(std::is_trivially_relocatable_v<T>) in the implementation of fbvector, and the programmer can stop writing explicit warrants. Finally, the programmer can start writing assertions of correctness, which aids maintainability and can even find real bugs. Example:

class Widget {
    std::vector<int> lst_;
};
static_assert(std::is_trivially_relocatable_v<Widget>);  // correctly SUCCEEDS

class Gadget {
    std::list<int> lst_;
};
static_assert(std::is_trivially_relocatable_v<Gadget>);  // correctly ERRORS OUT

The improvement in user experience for real-world codebases (such as [Folly], [EASTL], BDE, Qt, etc.) is the most important benefit to be gained by this proposal.

3. Design goals

Every C++ type already is or is not trivially relocatable. This proposal does not require any library vendor to make any library type trivially relocatable. (We assume that quality implementations will do so on their own.)

The optimizations above are in the domain of library writers. If you’re writing a vector, and you detect that your element type T is trivially relocatable, then whether you optimize in that case is up to you. This proposal does not require any library vendor to guarantee that any particular optimization happens. (But we assume that quality implementations will do so on their own.)

What C++ lacks is a standard way for library writers to detect the (existing) trivial relocatability of a type T, so that they can reliably apply their (existing) optimizations. All we really need is to add detection, and then all the optimizations described above will naturally emerge without any further special effort by WG21.

There are three kinds of object types that we want to make sure are correctly detected as trivially relocatable. These three cases are important for improving the performance of the standard library, and for improving the correctness of libraries such as [Folly]'s fbvector.

3.1. Standard library types such as std::string

In order to optimize std::vector<std::string>::resize, we must come up with a way to achieve

#include <string>
static_assert(is_trivially_relocatable< std::string >::value);
This could be done unilaterally by the library vendor — via a non-standard attribute ([[clang::trivially_relocatable]]), or a member typedef with a reserved name, or simply a vendor-provided specialization of std::is_trivially_relocatable<std::string>.

That is, we can in principle solve §3.1 while confining our "magic" to the headers of the implementation itself. The programmer doesn’t have to learn anything new, so far.

3.2. Program-defined types that follow the Rule of Zero

In order to optimize the SBO std::function in any meaningful sense, we must come up with a way to achieve

#include <string>
auto lam2 = [x=std::string("hello")]{};
static_assert(is_trivially_relocatable< decltype(lam2) >::value);
Lambdas are not a special case in C++; they are simply class types with all their special members defaulted. Therefore, presumably we should be able to use the same solution for lambdas as for
#include <string>
struct A {
    std::string s;
};
static_assert(is_trivially_relocatable< A >::value);
Here struct A follows the Rule of Zero: its move-constructor and destructor are both defaulted. If they were also trivial, then we’d be done. In fact they are non-trivial; and yet, because the type’s bases and members are all of trivially relocatable types, the type as a whole is trivially relocatable.

§3.2 asks that we make the static_assert succeed without breaking the "Rule of Zero." We do not want to require the programmer to annotate struct A with a special attribute, or a special member typedef, or anything like that. We want it to Just Work. Even for lambda types. This is a much harder problem than §3.1; it requires standard support in the core language. But it still does not require any new syntax.

3.3. Program-defined types with non-defaulted special members

In order to optimize std::vector<boost::shared_ptr<T>>::resize, we must come up with a way to achieve

struct B {
    B(B&&);  // non-trivial
    ~B();  // non-trivial
};
static_assert(is_trivially_relocatable< B >::value);
via some standard annotation applied to class type B (which in this example is standing in for boost::shared_ptr).

Note: We cannot possibly do it without annotation, because there exist examples of types that look just like B and are trivially relocatable (for example, libstdc++'s std::function) and there exist types that look just like B and are not trivially relocatable (for example, libc++'s std::function). The compiler cannot "crack open" the definitions of B(B&&) and ~B() to see if they combine to form a trivial operation: the definitions of B(B&&) and ~B() might not even be available in the current translation unit. So, without some kind of opt-in annotation, we cannot achieve our goal.

This use-case is the only one that requires us to design the "opt-in" syntax. In §3.1, any special syntax is hidden inside the implementation’s own headers. In §3.2, our design goal is to avoid special syntax. In §3.3, WG21 must actually design user-facing syntax.

Therefore, I believe it would be acceptable to punt on §3.3 and come back to it later. We say, "Sure, that would be nice, but there’s no syntax for it. Be glad that it works for core-language and library types. Ask again in three years." As long as we leave the design space open, I believe we wouldn’t lose much by delaying a solution to §3.3.

This paper does propose a standard syntax for §3.3 — an attribute — which in turn provides a simple and portable way for library vendors to implement §3.1.

4. Proposed language and library features

This paper proposes five separate additions to the C++ Standard. These additions introduce "relocate" as a well-supported C++ notion on par with "swap," and furthermore, successfully communicate trivial relocatability in each of the three use-cases above.

These five bullet points are severable to some degree. For example, if the [[trivially_relocatable]] attribute (point 5) is adopted, library vendors will certainly use it in their implementations; but if the attribute is rejected, library vendors could still indicate the trivial relocatability of certain standard library types by providing library specializations of is_trivially_relocatable (point 3).

Points 1 and 2 are completely severable from points 3, 4, and 5; but we believe these algorithms should be provided for symmetry with the other uninitialized-memory algorithms in the <memory> header (e.g. uninitialized_move) and the other trios of type-traits in the <type_traits> header (e.g. is_destructible, is_nothrow_destructible, is_trivially_destructible). I do not expect these templates to be frequently useful, but I believe they should be provided, so as not to surprise the programmer by their absence.

Points 3 and 4 together motivate point 5. In order to achieve the goal of § 3.2 Program-defined types that follow the Rule of Zero, we must define a core-language mechanism by which we can "inherit" trivial relocatability. This is especially important for the template case.

template<class T>
struct D {
    T t;
};

// class C comes in from outside, already marked, via whatever mechanism
constexpr bool c = is_trivially_relocatable< C >::value;
constexpr bool dc = is_trivially_relocatable< D<C> >::value;
static_assert(dc == c);
We strongly believe that std::is_trivially_relocatable<T> should be just a plain old class template, exactly like std::is_trivially_destructible<T> and so on. The core language should not know or care that the class template is_trivially_relocatable exists, any more than it knows that the class template is_trivially_destructible exists.

We expect that the library vendor will implement std::is_trivially_relocatable, just like std::is_trivially_destructible, in terms of a non-standard compiler builtin whose natural spelling is __is_trivially_relocatable(T). This builtin has been implemented in my fork of Clang; see [D50119]. The compiler computes the value of __is_trivially_relocatable(T) by inspecting the definition of T (and the definitions of its base classes and members, recursively). This recursive process "bottoms out" at primitive types, or at any type with a user-provided move or destroy operation. For safety, classes with user-provided move or destroy operations (e.g. Appendix C: Examples of non-trivially relocatable class types) must be assumed not to be trivially relocatable. To achieve the goal of § 3.3 Program-defined types with non-defaulted special members, we must provide a way that such a class can "opt in" and warrant to the implementation that it is in fact trivially relocatable (despite being non-trivially move-constructible and/or non-trivially destructible).

In point 5 we propose that the opt-in mechanism should be an attribute. The programmer of a trivially relocatable but non-trivially destructible class C will mark it for the compiler using the attribute:

struct [[trivially_relocatable]] C {
    C(C&&);  // defined elsewhere
    ~C(); // defined elsewhere
};
static_assert(is_trivially_relocatable< C >::value);
The attribute overrides the compiler’s usual computation.

An example of a "conditionally" trivially relocatable class is shown in Conditionally trivial relocation.

The attribute is severable; WG21 could adopt all the rest of this proposal and leave vendors to implement [[clang::trivially_relocatable]], [[gnu::trivially_relocatable]], etc., as non-standard extension mechanisms. In that case, we would strike all of §5.5 and one bullet point from §5.4; the rest of this proposal would remain exactly the same.

5. Proposed wording for C++2b

The wording in this section is relative to WG21 draft N4910.

5.1. Relocation operation [defns.relocation]

Add a new section in [intro.defs]:

relocation operation

the homogeneous binary operation performed by std::relocate_at, consisting of a move construction immediately followed by a destruction of the source object

this definition of "relocation operation" is not good

5.2. relocate_at and relocate [specialized.relocate]

Add a new section after [specialized.destroy]:

template<class T>
T *relocate_at(T* source, T* dest);

Mandates: T shall be a complete non-array object type.

Effects: Equivalent to:

struct guard { T *t; ~guard() { destroy_at(t); } } g(source);
return ::new (<i>voidify</i>(dest)) T(std::move(*source));

except that if T is trivially relocatable [basic.types], side effects associated with the relocation of the value of *source might not happen.

template<class T>
T relocate(T* source);

Mandates: T shall be a complete non-array object type.

Effects: Equivalent to:

T t = move(source);
destroy_at(source);
return t;

except that if T is trivially relocatable [basic.types], side effects associated with the relocation of the object’s value might not happen.

Note: These functions have both been implemented in my libc++ fork; for relocate, see godbolt.org/z/cqPP4oeE9 and [StdRelocateIsCute]. My implementation of their "as-if-by-memcpy" codepaths relies on UB, but Richard Smith confirmed (at Kona, February 2019) that a vendor could implement these codepaths by a call to some out_of_line_memcpy function whose implementation is invisible to the compiler’s optimizer. See @45:23–48:39 in my C++Now 2019 talk. The wording also deliberately permits a low-quality implementation with no such codepath at all.

5.3. uninitialized_relocate, uninitialized_relocate_n [uninitialized.relocate]

Add a new section after [uninitialized.move]:

template<class InputIterator, class NoThrowForwardIterator>
NoThrowForwardIterator uninitialized_relocate(InputIterator first, InputIterator last,
                                              NoThrowForwardIterator result);

Effects: Equivalent to:

try {
  for (; first != last; ++result, (void)++first) {
    ::new (<i>voidify</i>(*result))
      typename iterator_traits<NoThrowForwardIterator>::value_type(*first);
    destroy_at(addressof(*first));
  }
  return result;
} catch (...) {
  destroy(++first, last);
  throw;
}

except that if the iterators' common value type is trivially relocatable, side effects associated with the relocation of the object’s value might not happen.

Remarks: If an exception is thrown, all objects in both the source and destination ranges are destroyed.

template<class InputIterator, class Size, class NoThrowForwardIterator>
  pair<InputIterator, NoThrowForwardIterator>
    uninitialized_relocate_n(InputIterator first, Size n, NoThrowForwardIterator result);

Effects: Equivalent to:

try {
  for (; n > 0; ++result, (void)++first, --n) {
    ::new (<i>voidify</i>(*result))
      typename iterator_traits<NoThrowForwardIterator>::value_type(*first);
    destroy_at(addressof(*first));
  }
  return {first, result};
} catch (...) {
  destroy_n(++first, --n);
  throw;
}

except that if the iterators' common value type is trivially relocatable, side effects associated with the relocation of the object’s value might not happen.

Remarks: If an exception is thrown, all objects in both the source and destination ranges are destroyed.

Note: See [specialized.algorithms.general]/2 for the blanket wording that explains why objects in the destination range are destroyed, even though the "Equivalent to" wording doesn’t show them being destroyed. See the implementation in Appendix B: Sample code.

Note: We are guided by [P0884R0] to make uninitialized_relocate unconditionally noexcept(false). This is consistent with uninitialized_move and destroy_at, both of which are unconditionally noexcept(false).

5.4. Trivially relocatable type [basic.types.general]

Add a new section in [basic.types.general]:

A move-constructible, destructible object type T is a trivially relocatable type if it is:

  • a trivially copyable type, or

  • an array of trivially relocatable type, or

  • a (possibly cv-qualified) class type declared with a [[trivially_relocatable]] attribute with value true, or

  • a (possibly cv-qualified) class type which:

    • has no user-provided move constructors or move assignment operators,

    • has no user-provided copy constructors or copy assignment operators,

    • has no user-provided destructors,

    • has no virtual member functions,

    • has no virtual base classes,

    • all of whose members are either of reference type or of trivially relocatable type, and

    • all of whose base classes are trivially relocatable.

[Note: For a trivially relocatable type, the relocation operation (as performed by, for example, the library functions std::swap and std::vector::resize) is tantamount to a simple copy of the underlying bytes. —end note]

[Note: It is intended that most standard library types be trivially relocatable types. —end note]

Note: Since P1144R5, polymorphic types are not "naturally" trivially relocatable. See Appendix C, example 5.

Note: A class type declared [[trivially_relocatable(false)]] may still be trivially relocatable, if it satisfies the criteria above. For example, it is impossible to create a trivially copyable type which is not also trivially relocatable.

Note: There is no special treatment for volatile subobjects (see [Subobjects]) nor for possibly overlapping subobjects (see [Subobjects]).

The relevant move constructor, copy constructor, and/or destructor must be public and unambiguous. We imply this via the words "A move-constructible, destructible object type". However, "move-constructible" and "destructible" are library concepts, not core language concepts, so it is inappropriate to use them here.

5.5. [[trivially_relocatable]] attribute [dcl.attr.trivreloc]

Add a new section after [dcl.attr.nouniqueaddr]:

The attribute-token trivially_relocatable specifies that a class type’s relocation operation has no visible side-effects other than a copy of the underlying bytes, as if by the library function std::memcpy. It may be applied to the definition of a class. It shall appear at most once in each attribute-list. An attribute-argument-clause may be present and, if present, shall have the form

( constant-expression )
The constant-expression shall be an integral constant expression of type bool. If no attribute-argument-clause is present, it has the same effect as an attribute-argument-clause of (true).

If any definition of a class type has a trivially_relocatable attribute with value V, then each definition of the same class type shall have a trivially_relocatable attribute with value V. No diagnostic is required if definitions in different translation units have mismatched trivially_relocatable attributes.

If a type T is declared with the trivially_relocatable attribute, and T is either not move-constructible or not destructible, the program is ill-formed.

If a class type is declared with the trivially_relocatable attribute, and the program relies on observable side-effects of relocation other than a copy of the underlying bytes, the behavior is undefined.

"If a type T is declared with the trivially_relocatable attribute, and T is either not move-constructible or not destructible, the program is ill-formed." We might want to replace this wording with a mere "Note" encouraging implementations to diagnose. See this example where a diagnostic might be unwanted.

5.6. Type traits is_relocatable etc. [meta.unary.prop]

Add new entries to Table 47 in [meta.unary.prop]:

TemplateConditionPreconditions
template<class T> struct is_relocatable; is_move_constructible_v<T> is true and is_destructible_v<T> is true T shall be a complete type, cv void, or an array of unknown bound.
template<class T> struct is_nothrow_relocatable; is_relocatable_v<T> is true and both the indicated move-constructor and the destructor are known not to throw any exceptions. T shall be a complete type, cv void, or an array of unknown bound.
template<class T> struct is_trivially_relocatable; T is a trivially relocatable type. T shall be a complete type, cv void, or an array of unknown bound.

5.7. relocatable concept [concept.relocatable]

Add a new section after [concept.copyconstructible]:

template<class T>
  concept relocatable = move_constructible<T>;

If T is an object type, then let rv be an rvalue of type T, lv an lvalue of type T equal to rv, and u2 a distinct object of type T equal to rv. T models relocatable only if

  • After the definition T u = rv;, u is equal to u2.

  • T(rv) is equal to u2.

  • If the expression u2 = rv is well-formed, then the expression has the same semantics as u2.~T(); ::new ((void*)std::addressof(u2)) T(rv);

  • If the definition T u = lv; is well-formed, then after the definition u is equal to u2.

  • If the expression T(lv) is well-formed, then the expression’s result is equal to u2.

  • If the expression u2 = lv is well-formed, then the expression has the same semantics as u2.~T(); ::new ((void*)std::addressof(u2)) T(lv);

The semantic requirements of this concept are poorly worded. We intend that a type may be relocatable regardless of whether it is copy-constructible; but, if it is copy-constructible then copy-and-destroy must have the same semantics as move-and-destroy. We intend that a type may be relocatable regardless of whether it is assignable; but, if it is assignable then assignment must have the same semantics as destroy-and-copy or destroy-and-move.

Note: The semantic requirements on assignment help us optimize vector::insert and vector::erase. The type std::pmr::forward_list<int> satisfies relocatable, but it models relocatable only if all relevant objects have equal allocators.

Note: I do not propose to add a concept named trivially_relocatable. There is currently no concept named std::trivially_copy_constructible nor std::trivial.

6. Rationale and alternatives

6.1. Why not destructive move?

As discussed in EWGI at San Diego, this proposal does not give us a general user-provided "destructive move" facility.

6.2. Why [[trivially_relocatable(bool)]]?

It was suggested by numerous reviewers that [[trivially_relocatable]] should take an optional boolean parameter, as in [[trivially_relocatable(true)]]. This allows us to write complicated conditions directly inline, instead of using metaprogramming to inherit the right behavior from a conditional base class.

See Conditionally trivial relocation for an example of how this can be used.

There is no technical obstacle to adding an arbitrary C++ expression as the parameter to an attribute. The grammar for balancing [] and () in attribute parameters has been there since C++11. There is already prior art for arbitrary expressions in attributes; see Clang’s [[diagnose_if(bool)]] attribute. EWG has also previously considered an attribute [[assume_aligned(expr)]]; it was rejected in favor of [P1007R3]'s non-attribute approach, but not because of the (expr) part specifically.

The major downside I see to [[trivially_relocatable(expr)]] is that it could lead to an arbitrarily complicated C++ expression appearing in an awkward position. But this is better than having to do the metaprogramming tricks shown in Conditionally trivial relocation.

6.3. Attribute [[maybe_trivially_relocatable]]

The Clang patch currently available on Godbolt Compiler Explorer supports both [[clang::trivially_relocatable]] and another attribute called [[clang::maybe_trivially_relocatable]], which John McCall requested that I explore.

See P1144R4 section 6.2 for discussion of the [[clang::maybe_trivially_relocatable]] attribute, including the reasons I do not propose it for standardization.

6.4. Should relocate_at be a customization point?

No. See P1144R2 section 5.4 for discussion of this approach, which was taken by Pablo Halpern’s [N4158]. See also "Trivially Relocatable versus Destructive Movable" (2018-09-28).

N4158’s customization-point approach has a very high cost-to-benefit ratio. I am satisfied with P1144’s avoiding that approach.

6.5. Unintuitive is_nothrow_relocatable

Consider a type such as

struct [[trivially_relocatable]] Widget {
    int i;
    Widget(Widget&&) : i(rhs.i) {}
};
static_assert(not std::is_nothrow_move_constructible_v<Widget>);
static_assert(not std::is_nothrow_relocatable_v<Widget>);
static_assert(std::is_trivially_relocatable_v<Widget>);

Since Widget is non-nothrow move-constructible, P1144 calls it non-nothrow relocatable. So, looking at how Widget interacts with the type-traits, we are in the awkward position that Widget simultaneously claims "My relocation operation might throw" and "My relocation operation is trivial." These claims seem inconsistent.

This is a real-world concern because GNU libstdc++'s std::deque<T> works like Widget: its move-constructor is noexcept(false) (it must allocate) but it is trivially relocatable. As of 2019-01-18, libstdc++ marks its deque as trivially relocatable (see [Deque]).

However, I believe that it would be incorrect and unsafe for the library to claim that Widget was "nothrow relocatable." "Nothrow relocatable" should imply that a generic algorithm could relocate it (as if by std::uninitialized_relocate) without worrying about catching exceptions. "T is trivially relocatable" means that T is relocatable as if by memcpy; it does not mean that every relocation of T must be performed literally by memcpy.

I believe P1144’s proposed behavior is the best behavior. However, another plausible option would be simply to eliminate the is_nothrow_relocatable type-trait from the standard library. If we don’t provide is_nothrow_relocatable, then we don’t have to defend its mildly unintuitive behavior.

6.6. Confusing interactions with std::pmr and vector::insert

Note: This section was added in P1144R5.

The main thing P1144 does for std::vector is efficient reallocation of the whole buffer. However, a secondary goal is to permit efficient insertion and erasure. Example:

std::vector<std::string> vs(1000);
vs.erase(vs.begin()+500);

This erase requires us to shift down 500 std::string objects, which can be done either by 500 calls to string::operator= followed by one call to ~string, or by one call to ~string followed by a memcpy. We want to permit the latter. That’s why since P1144R3, the definition of "trivially relocatable" in § 5.4 Trivially relocatable type [basic.types.general] places requirements on T's assignment operators as well as on T's constructors and destructors.

However, consider std::pmr::vector<int>. Its move-constructor and destructor follow the rules for trivial relocatability, but its assignment operator does not.

std::vector<std::pmr::vector<int>> vv;
vv.emplace_back(1, 1, &mr1);
vv.emplace_back(2, 2, &mr2);
vv.emplace_back(3, 3, &mr3);
vv.reserve(1000);  // A
vv.erase(vv.begin());  // B

On line "A", we would like the implementation to use trivial relocation (memcpy) because it is fast and correct. But on line "B", if the implementation today uses operator= and tomorrow uses memcpy, the user will be able to detect the change by inspecting vv[0].get_allocator().

See this example completely worked out @76:17–84:20 in my C++Now 2019 talk.

For now, P1144R5 implies that std::pmr::vector<int> shall not be trivially relocatable, which means that line "A" will not get the memcpy speedup unless the vendor special-cases a whitelist of std::pmr containers. However, I think this area deserves more discussion.

Note: Regardless, std::pmr::polymorphic_allocator<int> is a trivially relocatable type. The problem above arises from propagate_on_move_assignment etc., which affect the behavior of operator= only for containers such as std::pmr::vector<int>. The author of the container makes the choice whether to respect POCMA/POCCA/POCS, and the author of the container also makes the choice when to warrant trivial relocatability. These choices are correlated, and so it is natural that they should be made by the same person, at the same place in the source code.

7. Acknowledgements

Thanks to Elias Kosunen, Niall Douglas, and John Bandela for their feedback on early drafts of this paper.

Many thanks to Matt Godbolt for allowing me to install the prototype Clang implementation on Compiler Explorer (godbolt.org). See also [Announcing].

Thanks to Nicolas Lesser for his relentless feedback on drafts of P1144R0, and for his helpful review comments on the Clang implementation [D50119].

Thanks to Howard Hinnant for appearing with me on [CppChat], and to Jon Kalb and Phil Nash for hosting us.

Thanks to Pablo Halpern for [N4158], to which this paper bears a striking resemblance —including the meaning assigned to the word "trivial," and the library-algorithm approach to avoiding the problems with "lame duck objects" discussed in the final section of [N1377]. See discussion of N4034 at Rapperswil (June 2014) and discussion of N4158 at Urbana (November 2014).

Significantly different approaches to this problem have previously appeared in Rodrigo Castro Campos’s [N2754], Denis Bider’s [P0023R0] (introducing a core-language "relocation" operator), and Niall Douglas’s [P1029R3] (treating trivial relocatability as an aspect of move-construction in isolation, rather than an aspect of the class type as a whole).

Thanks to John McCall for his thought-provoking review comments on the Clang implementation [D50119].

Thanks to Marc Glisse for his work integrating a "trivially relocatable" trait into GNU libstdc++ and for answering my questions on GCC bug 87106.

Thanks to Jens Maurer for his feedback on this paper at Kona 2019, and to Corentin Jabot for championing P1144R4 at Prague 2020.

Appendix A: Straw polls

The next time this paper is seen, Anton Zhilin would like us to take a straw poll on whether it should be possible to create a type which is "trivially relocatable, but not move-constructible." Currently P1144 does not permit such a thing; relocatability here is defined as a superset of move-constructibility.

Polls taken at EWGI at Prague on 2020-02-13

Corentin Jabot championed P1144R4. EWGI discussed P1144R4 and Niall Douglas’s [P1029R3] consecutively, then took the following straw polls. The final poll was interpreted by EWGI assistant chair Erich Keane as "weak consensus" to forward P1144 to EWG.

SF F N A SA
We believe that P1029 and P1144 are sufficiently different that they should be advanced separately. 7 3 2 0 0
EWGI is ok to have the spelling as an attribute with an expression argument. 3 5 1 1 0
EWGI would prefer a contextual keyword. 0 0 6 3 0
EWGI thinks the author should explore P1144 as a customizable type trait. 0 0 0 9 2
Forward P1144 to EWG. 1 3 4 1 0

Polls taken of the Internet between 2018-11-12 and 2018-11-21

SF F N A SA
We approve of the general idea that user-defined classes should be able to warrant their own trivial relocatability via a standard mechanism. 6 1 0 0 1
We approve of the general idea that user-defined classes which follow the Rule of Zero should inherit the trivial relocatability of their bases and members. 7 1 0 0 0
Nobody should be able to warrant the trivial relocatability of class C except for class C itself (i.e., we do not want to see a customization point analogous to std::hash). 4 2 2 0 0
A class should be able to warrant its own trivial relocatability via the attribute [[trivially_relocatable]], as proposed in this paper (P1144R0). 3 0 3 1 0
A class should be able to warrant its own trivial relocatability via some attribute, but not necessarily under that exact name. 2 0 4 1 0
A class should be able to warrant its own trivial relocatability as proposed in this paper (P1144R0), but via a contextual keyword rather than an attribute. 0 2 3 3 0
If a trait with the semantics of is_trivially_relocatable<T> is added to the <type_traits> header, the programmer should be permitted to specialize it for program-defined types (i.e., we want to see that trait itself become a customization point analogous to std::hash). 0 1 0 1 5
Trivial relocatability should be assumed by default. Classes such as those in Appendix C should indicate their non-trivial relocatability via an opt-in mechanism. 0 0 0 3 5
To simplify Conditionally trivial relocation, if an attribute with the semantics of [[trivially_relocatable]] is added, it should take a boolean argument. 1 1 3 2 0
The algorithm uninitialized_relocate(first, last, d_first) should be added to the <memory> header, as proposed in this paper (P1144R0). 0 4 1 1 0
The type trait is_relocatable<T> (and its _v version) should be added to the <type_traits> header, as proposed in this paper (P1144R0). 0 2 3 0 1
If is_relocatable<T> is added, then we should also add is_nothrow_relocatable<T> (and its _v version), as proposed in this paper (P1144R0). 1 4 2 0 0
The type trait is_trivially_relocatable<T> (and its _v version) should be added to the <type_traits> header, under that exact name, as proposed in this paper (P1144R0). 3 3 1 0 0
We approve of a trait with the semantics of is_trivially_relocatable<T>, but not necessarily under that exact name. (For example, is_bitwise_relocatable.) 3 3 0 1 0
If is_trivially_relocatable<T> is added, under that exact name, then the type trait is_trivially_swappable<T> (and its _v version) should also be added to the <type_traits> header. 0 3 3 0 0

The "Strongly Against" vote on poll 1 was due to concerns that P1144 permits a class to warrant its own trivial relocatability, overruling the compiler’s assumptions, not only when the compiler’s assumptions are based on the presence of special members, but also when the compiler’s assumptions are based partly or wholly on the non-triviality of member or base subobjects. See further discussion under § 6.3 Attribute [[maybe_trivially_relocatable]].

The "Against" vote on poll 10, uninitialized_relocate, was due to its exception guarantee, which was weaker in P1144R0. P1144R1 strengthened the guarantee (and tightened the constraint on the source iterator from InputIterator to ForwardIterator) to better match the other uninitialized_foo algorithms.

The "Strongly Against" vote on poll 11, is_relocatable, was from a desire to save the name relocatable for something different, such as a built-in destructive-move operation.

Poll taken of EWGI at San Diego on 2018-11-07

SF F N A SA
Should we commit additional committee time to solving the problem P1144R0 is trying to solve, knowing it will leave less time to other work? 8 3 0 0 0

Polls taken of SG14 at CppCon on 2018-09-26

SF F N A SA
The type trait is_trivially_relocatable<T> (and its _v version) should be added to the <type_traits> header, under that exact name, as proposed in this paper. 1 20 7 1 0
We approve of a trait with the semantics of is_trivially_relocatable<T>, but not necessarily under that exact name. (For example, is_bitwise_relocatable.) 15 12 1 0 0
We approve of the general idea that user-defined classes should be able to warrant their own trivial relocatability. 25 5 2 0 0

Appendix B: Sample code

Reference implementations of relocate, relocate_at, and uninitialized_relocate

See this C++20 code at godbolt.org/z/vMbTYrdrP.

template<class T>
T relocate(T *source)
    noexcept(std::is_nothrow_move_constructible_v<T>)
{
    struct Guard { T *t; ~Guard() { t->~T(); } };
    auto guard = Guard{source};
    return std::move(*source);
}

template<class T>
T relocate(T *source) noexcept
    requires std::is_trivially_relocatable_v<T>
{
    auto magic = (T(*)(T*, size_t))memcpy; // Itanium ABI only
    return magic(source, sizeof(T));
}

template<class T, class U>
U *relocate_at2(T *source, U *dest)
    noexcept(std::is_nothrow_constructible_v<U, T&&>)
{
    struct Guard { T *t; ~Guard() { t->~T(); } };
    auto guard = Guard{source};
    return ::new (dest) U(std::move(*source));
}

template<class T, class U>
U *relocate_at2(T *source, U *dest) noexcept
    requires std::is_same_v<const T, const U> && std::is_trivially_relocatable_v<T>
{
    std::memmove(dest, source, sizeof(T));
    return std::launder(dest);
}

template<class T>
T *relocate_at(T *source, T *dest) noexcept {
    return ::relocate_at2(source, dest);
}

template<class InputIterator, class ForwardIterator>
ForwardIterator uninitialized_relocate(
    InputIterator first, InputIterator last, ForwardIterator result)
{
    using T = typename std::iterator_traits<ForwardIterator>::value_type;
    using U = decltype(std::move(*first));
    constexpr bool memcpyable = (std::is_same_v<T, std::remove_reference_t<U>> && std::is_trivially_relocatable_v<T>);
    constexpr bool both_contiguous = (std::contiguous_iterator<InputIterator> && std::contiguous_iterator<ForwardIterator>);
    constexpr bool nothrow_relocatable = std::is_nothrow_constructible_v<T, U>;

    if constexpr (memcpyable && both_contiguous) {
        char *firstbyte = (char *)std::to_address(first);
        std::size_t nbytes = (char *)std::to_address(last) - firstbyte;
        if (nbytes != 0) {
            std::memmove(std::to_address(result), firstbyte, nbytes);
            result += (last - first);
        }
    } else if constexpr (nothrow_relocatable) {
        for (; first != last; (void)++result, ++first) {
            ::relocate_at2(std::addressof(*first), std::addressof(*result));
        }
    } else {
        ForwardIterator orig_result = result;
        for (; first != last; (void)++result, ++first) {
            try {
                ::relocate_at2(std::addressof(*first), std::addressof(*result));
            } catch (...) {
                ++first;
                std::destroy(first, last);
                std::destroy(orig_result, result);
                throw;
            }
        }
    }
    return result;
}

Conditionally trivial relocation

We expect, but do not require, that std::optional<T> should be trivially relocatable if and only if T itself is trivially relocatable.

The following abbreviated implementation shows how to achieve an optional<T> which has the same trivial-move-constructibility as T, the same trivial-destructibility as T, and the same trivial-relocatability as T.

The primitives of move-construction and destruction are provided by four specializations of optional_a; then the public optional extends the appropriate specialization of optional_a and applies a conditional [[trivially_relocatable]] attribute.

template<class T>
class [[trivially_relocatable(is_trivially_relocatable_v<T>)]] optional : optional_base<T>
{
    using optional_base<T>::optional_base;
};

template<class T, bool D = is_trivially_destructible_v<T>, bool M = is_trivially_move_constructible_v<T>>
class optional_base {
    // NOT SHOWN
};

I have implemented the entire Standard Library using the proposed [[trivially_relocatable]] attribute; you can find the source code on my GitHub and explore the resulting codegen on Godbolt Compiler Explorer.

I have also implemented case studies for std::optional and std::deque, in each of the alternative styles:

Style Size of optional diff (lines) Size of deque diff (lines)
[[trivially_relocatable]] -2, +14 -18, +52
[[maybe_trivially_relocatable]] problematic -5, +5
[[trivially_relocatable(bool)]] -1, +1 -1, +17

For why one entry in this table is "problematic," see § 6.3 Attribute [[maybe_trivially_relocatable]].

Appendix C: Examples of non-trivially relocatable class types

Class contains pointer to self

This fictional short_string illustrates a mechanism that can apply to any small-buffer-optimized class. libc++'s std::function uses this mechanism (on a 24-byte buffer) and is thus not trivially relocatable.

However, different mechanisms for small-buffer optimization exist. libc++'s std::any also achieves small-buffer optimization on a 24-byte buffer, without (necessarily) sacrificing trivial relocatability.

struct short_string {
    char *data_ = buffer_;
    size_t size_ = 0;
    char buffer_[8] = {};

    const char *data() const { return data_; }

    short_string() = default;
    short_string(const char *s) : size_(strlen(s)) {
        if (size_ < sizeof buffer_)
            strcpy(buffer_, s);
        else
            data_ = strdup(s);
    }
    short_string(short_string&& s) {
        memcpy(this, &s, sizeof(*this));
        if (s.data_ == s.buffer_)
            data_ = buffer_;
        else
            s.data_ = nullptr;
    }
    ~short_string() {
        if (data_ != buffer_)
            free(data_);
    }
};

Allocated memory contains pointer to self

std::list needs somewhere to store its "past-the-end" node, commonly referred to as the "sentinel node," whose prev pointer points to the list’s last node. If the sentinel node is allocated on the heap, then std::list can be trivially relocatable; but if the sentinel node is placed within the list object itself (as happens on libc++ and libstdc++), then relocating the list object requires fixing up the list’s last node’s next pointer so that it points to the new sentinel node inside the destination list object. This fixup of an arbitrary heap object cannot be simulated by memcpy.

Traditional implementations of std::set and std::map also store a "past-the-end" node inside themselves and thus also fall into this category.

struct node {
    node *prev_ = nullptr;
    node *next_ = nullptr;
};
struct list {
    node n_;
    iterator begin() { return iterator(n_.next_); }
    iterator end() { return iterator(&n_); }
    list(list&& l) {
        if (l.n_.next_) l.n_.next_->prev_ = &n_;  // fixup
        if (l.n_.prev_) l.n_.prev_->next_ = &n_;  // fixup
        n_ = l.n_;
        l.n_ = node{};
    }
    // ...
};

Class invariant depends on this

The offset_ptr provided by [Boost.Interprocess] is an example of this category.

struct offset_ptr {
    uintptr_t value_;

    uintptr_t here() const { return uintptr_t(this); }
    uintptr_t distance_to(void *p) const { return uintptr_t(p) - here(); }
    void *get() const { return (void*)(here() + value_); }

    offset_ptr() : value_(distance_to(nullptr)) {}
    offset_ptr(void *p) : value_(distance_to(p)) {}
    offset_ptr(const offset_ptr& rhs) : value_(distance_to(rhs.get())) {}
    offset_ptr& operator=(const offset_ptr& rhs) {
        value_ = distance_to(rhs.get());
        return *this;
    }
    ~offset_ptr() = default;
};

Program invariant depends on this

In the following snippet, struct Widget is relocatable, but not trivially relocatable, because the relocation operation of destroying a Widget at point A and constructing a new Widget at point B has behavior that is observably different from a simple memcpy.

std::set<void *> registry;

struct registered_object {
    registered_object() { registry.insert(this); }
    registered_object(registered_object&&) = default;
    registered_object(const registered_object&) = default;
    registered_object& operator=(registered_object&&) = default;
    registered_object& operator=(const registered_object&) = default;
    ~registered_object() { registry.erase(this); }
};

struct Widget : registered_object {};

Polymorphic downcast effectively relies on offset-into-self

Note: This section was added in P1144R5.

Thanks to David Stone for this example. In the following snippet, struct Base is relocatable, but not trivially relocatable, because its copy constructor and assignment operator do not copy the entire state of the right-hand object. (Notice that pf is initialized with f, not with a copy of o.pf.)

struct Base {
    static int f(Base*) { return 21; }
    int (*pf)(Base*);
    Base(int (*pf)(Base*) = f) : pf(pf) {}
    Base(const Base& o) : pf(f) {}
    Base& operator=(const Base&) { return *this; }
};
struct Derived : Base {
    static int f(Base *self) { return ((Derived*)self)->x; }
    Derived() : Base(f) {}
    Derived(const Derived&) = default;
    Derived& operator=(const Derived& o) { x = o.x; return *this; }
    int x = 42;
};

int main() {
    Base&& d = Derived();
    Base&& b = Base();
    std::swap(b, d);
    printf("%d\n", b.pf(&b));
}

The above snippet is isomorphic to a classically polymorphic hierarchy with virtual methods. Here is the same snippet using virtual:

struct Base {
    virtual int f() { return 21; }
};
struct Derived : Base {
    int f() override { return x; }
    int x = 42;
};

int main() {
    Base&& b = Base();
    Base&& d = Derived();
    std::swap(b, d);
    printf("%d\n", b.f());
}

This is why (since P1144R5) the compiler will not consider types with virtual methods to be "naturally" trivially relocatable.

Appendix D: Implementation experience

A prototype Clang/libc++ implementation is at

Side-by-side case studies of [[trivially_relocatable]], [[maybe_trivially_relocatable]], and [[trivially_relocatable(bool)]] are given in Conditionally trivial relocation.

As of November 2018, libstdc++ performs the vector::resize optimization for any type which has manually specialized std::__is_bitwise_relocatable. (See this Godbolt.) Manual specialization is also the approach used by [EASTL], [Folly], and [BSL]. As of 2022-06-01, the only libstdc++ library type for which __is_bitwise_relocatable has been specialized is deque; see [Deque] and § 6.5 Unintuitive is_nothrow_relocatable.

As of April 2022, Clang trunk provides a compiler builtin type trait __is_trivially_relocatable(T) (see [D114732]), which is largely the same as the trait proposed in this paper. (There are slight differences; e.g., Clang reports polymorphic types, reference types, and incomplete types as trivially relocatable, whereas P1144 does not. I’m not aware that any of Clang’s differences were intentional.) Clang trunk doesn’t provide any equivalent of the [[trivially_relocatable]] attribute, so __is_trivially_relocatable(T) is true only for types T that are "naturally" trivially relocatable, such as trivially copyable types and types marked with the [[clang::trivial_abi]] attribute. As of 2022-06-10, Clang trunk has no conception of a type which is non-trivial for purposes of calls and yet is trivially relocatable. But Clang’s current status is compatible with P1144 (modulo the few unintentional differences in __is_trivially_relocatable mentioned above).

Index

Terms defined by this specification

References

Normative References

[N4910]
Thomas Köppe. Working Draft, Standard for Programming Language C++. 17 March 2022. URL: https://wg21.link/n4910

Informative References

[Announcing]
Arthur O'Dwyer. Announcing "trivially relocatable". July 2018. URL: https://quuxplusone.github.io/blog/2018/07/18/announcing-trivially-relocatable/
[Bench]
Arthur O'Dwyer. Benchmark code from "The Best Type Traits C++ Doesn't Have". April 2018. URL: https://github.com/Quuxplusone/from-scratch/blob/095b246d/cppnow2018/benchmark-relocatable.cc
[Boost.Interprocess]
Ion Gaztañaga. Mapping Address Independent Pointer: offset_ptr. 2005. URL: https://www.boost.org/doc/libs/1_67_0/doc/html/interprocess/offset_ptr.html
[BSL]
Bloomberg. bslmf::IsBitwiseMoveable: bitwise moveable trait metafunction. 2013–2017. URL: https://github.com/bloomberg/bde/blob/master/groups/bsl/bslmf/bslmf_isbitwisemoveable.h#L17
[Contra]
Arthur O'Dwyer. Contra built-in library types. April 2018. URL: https://quuxplusone.github.io/blog/2018/04/15/built-in-library-types/
[CppChat]
Howard Hinnant; Arthur O'Dwyer. cpp.chat episode 40: It works but it's undefined behavior. August 2018. URL: https://www.youtube.com/watch?v=8u5Qi4FgTP8
[D114732]
Devin Jeanpierre. [clang] Mark trivial_abi types as trivially relocatable. November 2021. URL: https://reviews.llvm.org/D114732
[D50119]
Arthur O'Dwyer; Nicolas Lesser; John McCall. Compiler support for P1144R0 __is_trivially_relocatable(T). July 2018. URL: https://reviews.llvm.org/D50119
[Deque]
Marc Glisse. Improve relocation ... (__is_trivially_relocatable): Specialize for deque. November 2018. URL: https://github.com/gcc-mirror/gcc/commit/a9b9381580de611126c9888c1a6c12a77d9b682e
[EASTL]
Paul Pedriana. N2271: EASTL — Electronic Arts Standard Template Library. URL: http://www.open-std.org/jtc1/sc22/wg21/docs/papers/2007/n2271.html#type_traits_extensions
[FixedCapacityVector]
Arthur O'Dwyer. P1144 case study: Moving a `fixed_capacity_vector`. URL: https://quuxplusone.github.io/blog/2019/02/22/p1144-fixed-capacity-vector/
[Folly]
Facebook. Folly documentation on "Object Relocation". URL: https://github.com/facebook/folly/blob/master/folly/docs/FBVector.md#object-relocation
[FollyIssue889]
Arthur O'Dwyer. Traits.h marks std::list as trivially relocatable, but in fact it is not. URL: https://github.com/facebook/folly/issues/889
[LibcxxAny]
Eric Fiselier. libc++ implementation of std::any (trivially relocatable). July 2016. URL: https://github.com/llvm-mirror/libcxx/blob/8fdc4918/include/any#L389-L394
[LibcxxFunction]
Howard Hinnant et al. libc++ implementation of std::function (non-trivially relocatable). URL: https://github.com/llvm-mirror/libcxx/blob/4e7ffcaa/include/functional#L1719-L1734
[LibstdcxxFunction]
Doug Gregor et al. libstdc++ implementation of std::function (trivially relocatable). URL: https://github.com/gcc-mirror/gcc/blob/master/libstdc%2B%2B-v3/include/bits/std_function.h
[N1377]
Howard Hinnant; Peter Dimov; Dave Abrahams. N1377: A Proposal to Add Move Semantics Support to the C++ Language. September 2002. URL: http://www.open-std.org/jtc1/sc22/wg21/docs/papers/2002/n1377.htm
[N2754]
Rodrigo Castro Campos. N2754: TriviallyDestructibleAfterMove and TriviallyReallocatable (rev 3). September 2008. URL: http://www.open-std.org/jtc1/sc22/wg21/docs/papers/2008/n2754.html
[N4158]
Pablo Halpern. N4158: Destructive Move (rev 1). October 2014. URL: http://www.open-std.org/jtc1/sc22/wg21/docs/papers/2014/n4158.pdf
[P0023R0]
Denis Bider. P0023R0: Relocator: Efficiently Moving Objects. April 2016. URL: http://open-std.org/JTC1/SC22/WG21/docs/papers/2016/p0023r0.pdf
[P0884R0]
Nicolai Josuttis. P0884R0: Extending the noexcept Policy, Rev. 0. February 2018. URL: http://www.open-std.org/jtc1/sc22/wg21/docs/papers/2018/p0884r0.pdf
[P1007R3]
Timur Doumler; Chandler Carruth. P1007R3: std::assume_aligned. November 2018. URL: http://www.open-std.org/jtc1/sc22/wg21/docs/papers/2018/p1007r3.pdf
[P1029R3]
Niall Douglas. P1029R3: move = bitcopies. January 2020. URL: http://www.open-std.org/jtc1/sc22/wg21/docs/papers/2020/p1029r3.pdf
[StdRelocateIsCute]
Arthur O'Dwyer. std::relocate's implementation is cute. May 2022. URL: https://quuxplusone.github.io/blog/2022/05/18/std-relocate/
[Subobjects]
Arthur O'Dwyer. When is a trivially copyable object not trivially copyable?. July 2018. URL: https://quuxplusone.github.io/blog/2018/07/13/trivially-copyable-corner-cases/

Issues Index

this definition of "relocation operation" is not good
The relevant move constructor, copy constructor, and/or destructor must be public and unambiguous. We imply this via the words "A move-constructible, destructible object type". However, "move-constructible" and "destructible" are library concepts, not core language concepts, so it is inappropriate to use them here.
"If a type T is declared with the trivially_relocatable attribute, and T is either not move-constructible or not destructible, the program is ill-formed." We might want to replace this wording with a mere "Note" encouraging implementations to diagnose. See this example where a diagnostic might be unwanted.
The semantic requirements of this concept are poorly worded. We intend that a type may be relocatable regardless of whether it is copy-constructible; but, if it is copy-constructible then copy-and-destroy must have the same semantics as move-and-destroy. We intend that a type may be relocatable regardless of whether it is assignable; but, if it is assignable then assignment must have the same semantics as destroy-and-copy or destroy-and-move.
The next time this paper is seen, Anton Zhilin would like us to take a straw poll on whether it should be possible to create a type which is "trivially relocatable, but not move-constructible." Currently P1144 does not permit such a thing; relocatability here is defined as a superset of move-constructibility.