1. Introduction
There is ongoing disagreement and confusion around the phrase "trivially relocatable", as some C++ programmers feel that it should intuitively mean that can be relocated by simply doing a raw byte-by-byte copy from one memory location to another (for example by using , , or a simple loop with pointers).
This proposal removes the phrase "trivially relocatable" from the Standard and replaces it with an integer rating for relocatability. The following 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 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 the object’s bytes, while other committee members allow for the invocation of implementation-specific relocation algorithms (for example on the 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 is in Category 1 if the class has been defined with the attribute , for example:
class T [[ reloc_memcpy ]] { };
The class can still be in Category 1 even if not defined with the attribute , but only if all of the following criteria are met:
-
does not have a custom-implemented move-constructorT -
does not have a custom-implemented copy-constructorT -
All of
’s member variables (if any) are Category 1T -
All of
’s base classes (if any) are Category 1T
4. Category 2 - compiler built-in
All Category 2 types are classes.
The only platform that has Category 2 types is .
On , 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 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 is in one of the categories 4 - 7 if it satisfies all of the following criteria:
-
does not have a custom relocater (i.e.T )T :: std_relocate -
is not defined with the attributeT [[ reloc_memcpy ]] -
is not trivially copyableT
7. Proposed API
namespace std { template < typename T , bool disregard_custom = false> consteval int relocatability ( void ) noexcept { return . . . ; } }
The second template parameter, , can be used to disregard the fact that the class has a static member function named , 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 , 1u z )) ? 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
- Integer classification provides an ordered preference for relocation strategies.
- Signedness conveys exception-safety (positive =
).noexcept - Custom
allows user-defined relocation semantics.T :: std_relocate - Compiler built-ins (category 2) allow progressive adoption of new implementation-specific algorithms.
10. Impact on the Standard
This proposal introduces a single, header-only library extension (), 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 architecture. According to P2786, here is how 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 = 0u z ; i < n ; ++ i ) restart_lifetime ( dst [ i ] ); }
The idea here as described in P2786 is that would be a no-op on all architectures except for 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 would accommodate these futuristic technologies.
This paper however proposes that 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 . On , 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 , this paper proposes that this will all be handled in .
Scroll down further to see how this would affect the implementation of .
12. What about std :: relocate ?
If the Standard were to have added to the standard library, and to have removed, then a possible implementation of 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 = 0u z ; 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 = 0u z ; 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 = 0u z ; 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 = 0u z ; 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 = 0u z ; 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 = 0u z ; i < n ; ++ i ) { construct_at ( dst + i ); dst [ i ] = src [ i ]; destroy_at ( src + i ); } } }