This page is a snapshot from the LWG issues list, see the Library Active Issues List for more information and the meaning of New status.

4283. std::trivially_relocate needs stronger preconditions on "nested" objects with dynamic lifetime

Section: 20.2.6 [obj.lifetime] Status: New Submitter: Giuseppe D'Angelo Opened: 2025-06-23 Last modified: 2025-06-28

Priority: Not Prioritized

View other active issues in [obj.lifetime].

View all other issues in [obj.lifetime].

View all issues with New status.

Discussion:

In 20.2.6 [obj.lifetime] the std::trivially_relocate function is missing a precondition, that is, that any object alive in the range being relocated is itself trivially relocatable.

We know the objects in the range are trivially relocatable, because there is a Mandates: element for this. The current draft has precise rules to determine whether a type is trivially relocatable or not; in general, subobjects are considered there (cf. 11.2 [class.prop], "eligible for trivial relocation", which discusses base classes and non-static data members).

However these rules do not take into account objects with dynamic lifetime whose storage is being provided by (sub)objects in the range.

For instance, given a wrapper type like:

// wraps a T
template<typename T>
struct wrapper {
  alignas(T) std::byte data[sizeof(T)];
};

then one can build a non-trivially relocatable object into wrapper objects:

struct NTR { ~NTR() {} };
static_assert(not std::is_trivially_relocatable_v<NTR>);

using WS = wrapper<NTR>;
static_assert(std::is_trivially_relocatable_v<WS>); // OK

And now one can do this:

WS* ws = /* … */;     // create a wrapper
new (&ws->data) NTR();  // create a NTR object into it

std::trivially_relocate(ws, ws+1, dest); // should be UB

Attempting to trivially relocate *ws should result in undefined behavior because NTR isn't trivially relocatable. I don't believe that this fact is correctly captured by the preconditions of std::trivially_relocate.

A similar issue is present for polymorphic types. In P2786's design polymorphic types can be trivially relocatable (assuming all the other conditions hold). Given a trivially relocatable polymorphic type P, then this code:

struct P { virtual void f(); };
static_assert(std::is_trivially_relocatable_v<P>);

using WP = wrapper<P>;
WP* wp = /* … */;   // create a wrapper
new (&wp->data) P();  // create a P object into it

std::trivially_relocate(wp, wp+1, dest); // implementation defined

is well-defined or UB, depending on the implementation. This is because on some implementations trivially relocating a polymorphic type requires patching its virtual table pointer; cf. the discussion in chapter 15.1 of P2786R13. However the "type erasure" done by wrapper<P> in the example (ultimately, it is just an array of bytes) does not allow implementations to do such patching, and the code is going to fail at runtime. Therefore this case also needs to be discussed by std::trivially_relocate's specification.

Proposed resolution:

This wording is relative to N5008.

  1. Modify 20.2.6 [obj.lifetime] as indicated:

    [Drafting note: For the general part of the issue (all objects in the range must be of trivially relocatable type), we append another point at the end of the existing Preconditions: element of trivially_relocate.
    For the specifics of polymorphic types, we amend at the end of the description the existing Remarks: element]

    template<class T>
      T* trivially_relocate(T* first, T* last, T* result);
    

    -9- Mandates: […]

    -10- Preconditions:

    1. (10.1) — [first, last) is a valid range.

    2. (10.2) — [result, result + (last - first)) denotes a region of storage that is a subset of the region reachable through result (6.8.4 [basic.compound]) and suitably aligned for the type T.

    3. (10.3) — No element in the range [first, last) is a potentially-overlapping subobject.

    4. (10.?) — All objects whose storage is being provided for (6.7.2 [intro.object]) by objects in the [first, last) range are of trivially relocatable type.

    -11- Postconditions: […]

    -12- Returns: result + (last - first).

    -13- Throws: Nothing.

    -14- Complexity: Linear in the length of the source range.

    -15- Remarks: The destination region of storage is considered reused (6.7.4 [basic.life]). No constructors or destructors are invoked. If any polymorphic object (11.7.3 [class.virtual]) exists in storage provided for (6.7.2 [intro.object]) by objects in the [first, last) range, it is implementation-defined whether the behavior is undefined.

    [Note 2: Overlapping ranges are supported. — end note]