PxxxxR4
consteval int relocatability

Draft Proposal,

Author:
Thomas PK Healy (healy8tpk@vir8jacode.com -- Remove all 8's)
Project:
ISO/IEC 14882 Programming Languages — C++, ISO/IEC JTC1/SC22/WG21

Abstract

Remove all mentions of "trivially relocatable" from the C++ Standard and replace with an integer rating for relocatability.

1. Introduction

There is ongoing disagreement and confusion around the phrase "trivially relocatable", as some C++ programmers feel that it should intuitively mean that T can be relocated by simply doing a raw byte-by-byte copy from one memory location to another (for example by using memcpy, memmove, realloc or a simple loop with char unsigned pointers).

This proposal removes the phrase "trivially relocatable" from the Standard and replaces it with an integer rating for relocatability. The following consteval template function shall be added to the standard library:

namespace std {
    template<typename T, bool disregard_custom = false>
    consteval int relocatability(void) noexcept
    {
        return  . . .  ;
    }
}

The integer returned from this function can have thirteen possible values:

 0 : cannot relocate
 1 : memcpy/memmove
 2 : implementation-specific built-in compiler algorithm that never fails
 3 : static member function (T::std_relocate) that never throws
-3 : static member function (T::std_relocate) that might throw
 4 : nothrow-move, destroy original
-4 : move (might throw), destroy original
 5 : nothrow-copy, destroy original
-5 : copy (might throw), destroy original
 6 : nothrow-default-construct, nothrow-move-assign, destroy original
-6 : default-construct (might throw), move-assign (might throw), destroy original
 7 : nothrow-default-construct, nothrow-copy-assign, destroy original
-7 : default-construct (might throw), copy-assign (might throw), destroy original

These values are available as constexpr variables:

namespace std {
    constexpr int reloc0_cannot       =  0;
    constexpr int reloc1_memcpy       =  1;
    constexpr int reloc2_compiler     =  2;
    constexpr int reloc3_custom_impl  =  3; constexpr int reloc3throw_custom_impl  =  -3;
    constexpr int reloc4_move         =  4; constexpr int reloc4throw_move         =  -4;
    constexpr int reloc5_copy         =  5; constexpr int reloc5throw_copy         =  -5;
    constexpr int reloc6_move_assign  =  6; constexpr int reloc6throw_move_assign  =  -6;
    constexpr int reloc7_copy_assign  =  7; constexpr int reloc7throw_copy_assign  =  -7;
}

2. Motivation

Papers such as P3780 (Giuseppe D’Angelo) illustrate ongoing disagreement within the committee about the precise semantics of "trivial relocation". Some committee members feel that 'trivial' should mean that we can memcpy the object’s bytes, while other committee members allow for the invocation of implementation-specific relocation algorithms (for example on the arm64e architecture, the re-encrypting of vtable pointers). These differing views hinder standardisation.

A numeric classification removes ambiguity by explicitly stating how a type can be relocated, instead of a binary yes/no notion.

3. Category 1 - memcpy

All non-class types are Category 1.

The class T is in Category 1 if the class has been defined with the attribute [[reloc_memcpy]], for example:

class T [[reloc_memcpy]] {
};

The class T can still be in Category 1 even if not defined with the attribute [[reloc_memcpy]], but only if all of the following criteria are met:

4. Category 2 - compiler built-in

All Category 2 types are classes.

The only platform that has Category 2 types is arm64e.

On arm64e, all polymorphic class objects are Category 2, reason being that the object’s vtable pointer is encrypted. When an object is relocated to a different address in memory, a compiler built-in algorithm must re-encrypt the vtable pointer.

5. Category 3 - custom relocator

All Category 3 types are classes.

A class implements its own custom relocation algorithm by providing a static member function named std_relocate with the following signature:

void std_relocate(T *dest, T const *src, std::size_t n) noexcept

Here is an example of a class which provides its own relocation algorithm:

struct File {
    int fd = -1;
    unsigned long relocation_count = 0u;
    ...
    ...
    File(void  ) = default;
    File(File& ) = delete;
    File(File&&) = delete;
    static void std_relocate(File *const dest, File const *const src, std::size_t const n) noexcept
    {
        std::memcpy( dest, src, n * sizeof(*dest) );
        . . .
        . . .
        for ( std::size_t i = 0u; i < n; ++i )
            dest[i].relocation_count += 1u;
    }
};

template<typename T>
requires ( 0 < std::relocatability<  std::remove_cvref_t<T>  >() )
void MyFunction(T &&arg)
{
    // Works only for non-throwing relocatable types
}

static_assert(  3 == std::relocatability<File>()  );

int main(void)
{
    File f;
    MyFunction(f);    
}

6. Categories 4 - 7

All Category 4 - 7 types are classes.

The class T is in one of the categories 4 - 7 if it satisfies all of the following criteria:

7. Proposed API

namespace std {
    template<typename T, bool disregard_custom = false>
    consteval int relocatability(void) noexcept
    {
        return  . . .  ;
    }
}

The second template parameter, disregard_custom, can be used to disregard the fact that the class has a static member function named std_relocate, which can be useful to determine inside the body of a custom relocator function whether or not a compiler built-in algorithm is required to be invoked:

struct T {
    static void std_relocate(T *const dest, T const *const src, std::size_t const n) noexcept
    {
        if constexpr ( 1 == std::relocatability<T, true> )
        {
            std::memcpy( dest, src, n * sizeof(*dest) );
            . . .
            . . .
        }
    }
};

8. Implementation

A possible implementation:

template<typename T, bool disregard_custom = false>
consteval int relocatability(void) noexcept
{
    if constexpr ( !disregard_custom && requires(T *dst, T const *src, std::size_t n){ T::std_relocate(dst, src, n); } )
    {
        return noexcept(T::std_relocate((T*)nullptr,(T*)nullptr,1uz)) ? 3 : -3;
    }
#ifdef __ARM64E__
    else if constexpr ( __builtin_arm64e_query_only_resign_vptr_to_reloc(T) ) return 2;
#elifdef __SOME_FUTURISTIC_CPU__
    else if constexpr ( __builtin_futuristic_query_something(T) ) return 2;
#endif
    else if constexpr ( is_trivially_copyable_v<T>  ) return 1; // memcpy/memmove
    else if constexpr ( is_destructible_v<T> )
    {
        /**/ if constexpr ( is_nothrow_move_constructible_v<T>  ) return  4;
        else if constexpr ( is_move_constructible_v<T>          ) return -4;
        else if constexpr ( is_nothrow_copy_constructible_v<T>  ) return  5;
        else if constexpr ( is_copy_constructible_v<T>          ) return -5;
        else if constexpr ( is_nothrow_default_constructible_v<T> && is_nothrow_move_assignable_v<T> ) return 6;
        else if constexpr ( is_default_constructible_v<T> && is_move_assignable_v<T> ) return -6;
        else if constexpr ( is_nothrow_default_constructible_v<T> && is_nothrow_copy_assignable_v<T> ) return 7;
        else if constexpr ( is_default_constructible_v<T> && is_copy_assignable_v<T> ) return -7;
    }
    else return 0;
}

9. Design Considerations

10. Impact on the Standard

This proposal introduces a single, header-only library extension (<type_traits>), adds no new keywords, and provides a clear replacement for the ambiguous term "trivially relocatable". It has no breaking changes -- all pre-existing code will be unaffected.

11. What about std::restart_lifetime?

Proposal paper P2786 has devised a way to accommodate the relocating of polymorphic objects on the arm64e architecture. According to P2786, here is how restart_lifetime would be used:

template<typename T>
void trivially_relocate(T *const dst, T const *const src, size_t const n)
{
    memmove( dst, src, n * sizeof(*dst) );
    for ( unsigned i = 0uz; i < n; ++i ) restart_lifetime( dst[i] );
}

The idea here as described in P2786 is that restart_lifetime would be a no-op on all architectures except for arm64e where it would re-encrypt the vtable pointer. In the future as more sophisticated CPU’s are designed with extra instructions for code safety, the idea is that restart_lifetime would accommodate these futuristic technologies.

This paper however proposes that restart_lifetime would be removed from the Standard, primarily because it is not future-proof; its declaration is as follows:

template<typename T>
T *restart_lifetime(T *p) noexcept;

This function is a no-op on all platforms except for arm64e. On arm64e, the object’s new address is enough information to correctly re-encrypt the vtable pointer. However in the future, there might be an architecture with a more complicated pointer authentication system, and the re-encrypting of the vtable pointer might also require the object’s old address. And so the declaration would need to change to:

template<typename T>
T *restart_lifetime(T *old, T *new) noexcept;

Rather than handle these complications in restart_lifetime, this paper proposes that this will all be handled in std::relocate.

Scroll down further to see how this would affect the implementation of std::relocate.

12. What about std::relocate?

If the Standard were to have std::relocatability added to the standard library, and to have restart_lifetime removed, then a possible implementation of std::relocate would be as follows:

template<typename T>
void relocate(T *const dst, T const *const src, size_t const n)
{
    static_assert( 0 != relocatability<T>() );

    size_t const distance = n * sizeof(*dst);

    T const *const low  = (dst > src) ? src : dst,
            *const high = (dst > src) ? dst : src;

    if constexpr ( 1 == relocatability<T>() )
    {
        if ( (low + n) < high ) memmove(dst,src,distance);
        else memcpy(dst,src,distance);
    }
#ifdef __ARM64E__
    else if constexpr ( 2 == relocatability<T>() )
    {
        if ( (low + n) < high ) memmove(dst,src,distance);
        else memcpy(dst,src,distance);
        for ( unsigned i = 0uz; i < n; ++i )
            __builtin_arm64e_resign_vptr( dst[i] );
    }
#elifdef __SOME_FUTURISTIC_CPU__
    else if constexpr ( 2 == relocatability<T>() )
    {
        memmove( dst, src, n * sizeof(*dst) );
        for ( unsigned i = 0uz; i < n; ++i )
            __builtin_futuristic_algorithm( &src[i], &dst[i] );
    }
#endif
    else if constexpr ( 3 == relocatability<T>() || -3 == relocatability<T>() )
    {
         T::std_relocate(dst, src, n);
    }
    else if constexpr ( 4 == relocatability<T>() || -4 == relocatability<T>() )
    {
        for ( size_t i = 0uz; i < n; ++i )
        {
            construct_at( dst + i, move(src[i]) );
            destroy_at( src + i );
        }
    }
    else if constexpr ( 5 == relocatability<T>() || -5 == relocatability<T>() )
    {
        for ( size_t i = 0uz; i < n; ++i )
        {
            construct_at( dst + i, src[i] );
            destroy_at( src + i );
        }
    }
    else if constexpr ( 6 == relocatability<T>() || -6 == relocatability<T>() )
    {
        for ( size_t i = 0uz; i < n; ++i )
        {
            construct_at( dst + i );
            dst[i] = move( src[i] );
            destroy_at( src + i );
        }
    } 
    else if constexpr ( 7 == relocatability<T>() || -7 == relocatability<T>() )
    {
        for ( size_t i = 0uz; i < n; ++i )
        {
            construct_at( dst + i );
            dst[i] = src[i];
            destroy_at( src + i );
        }
    } 
}