[...]
As an optimisation for B, I want to use 'memcpy' instead of running a
loop with 'construct_at'. But in order to be able to determine when I
can use this optimisation, I need a new trait. Right now the Standard
only has 'is_trivially_copyable' for this purpose
Wrong.
C++ has many "is_trivially_fooable" type traits, for various verbs "foo". They come in two flavors:
- Surgical traits: is_trivially_copy_constructible, is_trivially_move_assignable, is_trivially_destructible, (
Clang's is_trivially_equality_comparable,
Giuseppe's is_trivially_value_initializable, ...)
- Holistic traits: is_trivially_copyable, (
P1144's is_trivially_relocatable)
The "surgical" traits are — well, caveat: they're currently not exactly, but they're basically intended to have been and are currently used in the wild as — simple optimization gates. They're aimed at the user-programmer who says, "Okay, my code wants to do this specific operation on a type (e.g., copy-construct it; e.g. compare it for equality). Can I get away with doing that specific operation on the object representation (e.g. memcpy it; e.g. memcmp it) instead?" The poster children for these traits are all in the STL:
- is_trivially_copy_assignable controls whether std::copy can memcpy
- is_trivially_move_assignable controls whether
std::move can memcpy
- is_trivially_copy_constructible controls whether std::uninitialized_copy can memcpy
- is_trivially_move_constructible controls whether std::uninitialized_move can memcpy
- is_trivially_equality_comparable controls whether std::equal can memcmp
- is_trivially_default_constructible controls whether std::uninitialized_default_construct can memset
- is_trivially_value_initializable controls whether std::uninitialized_value_construct can memset
In your case, you are precisely asking "Can I replace copy-construction with memcpy?" So you should ask is_trivially_copy_constructible_v<T>.
(Now, currently is_trivially_copy_constructible_v<T> reports false for types with non-trivial destructors. This is the subject of
LWG2827 and
P2842R0, and falls under my "currently not exactly" caveat.)
The "holistic" traits capture complex holistic properties of a type. Right now there's only one — is_trivially_copyable. This is aimed at the user-programmer who says, "Okay, my code wants to do some complicated rearrangement, copying, and destruction of a bunch of objects, and I don't want to restrict myself as to exactly what specific operations I'm going to pretend to do. I just want to know, can I get away with doing such a complicated operation on the object representations of these objects?" For example, I'm going to do something like std::partial_sort_copy — can I copy and shuffle the objects as if they were just bags of bytes? is_trivially_copyable tells me the answer.
is_trivially_relocatable (non-standard, but used by many libraries in real life, including Abseil, Folly, HPX, Parlay, and Thrust) answers the same question but specifically for complicated affine (one-to-one) rearrangements of values. For example, I'm going to std::sort these objects, or std::rotate them, or std::partition them — can I permute their object representations instead of calling their assignment operators and swap and so on? I'm going to reallocate this buffer of objects from buffer A to buffer B — can I memcpy them instead of calling their move constructors and destructors? I'm going to std::partial_sort_copy these objects from A to B, and then destroy from A exactly those objects whose values I've copied into B — can I do that just by shuffling bytes? is_trivially_relocatable tells me the answer.
In particular, is_trivially_relocatable tells you whether you can safely lower std::sort to std::qsort (although it doesn't help with the icky impedance mismatch between sort's and qsort's comparator signatures).
As for the name of this new trait, well the word 'trivial' is no
longer usable in any context in C++ because of the extreme ambiguity
that has been given to it -- remember how we were gonna say an object
was 'trivially relocatable' even if you needed to run an encryption
algorithm after relocating it? And we called that encryption algorithm
"restart_lifetime"? So forget about the word 'trivial', it's useless
now.
FWIW, I disagree; I think the Kona vote proved that an overwhelming majority of WG21 rejected the attempt to make "trivial" mean something other than what it means; if anything, we can say that the meaning it's already got in popular usage has been recently reaffirmed.
[...] Let's call this new trait: is_memcpyable
No. That's like calling a mushroom is_edible ("Any mushroom is edible... once"), or a spirit from the vasty deep is_callable ("But will they come when you do call for them?"). Any type is memcpyable; but what does it do when you do memcpy it?
If memcpying it is a practical substitute for copy-constructing it, then it is trivially copy constructible.
If memcpying it is a practical substitute for move-assigning it, then it is trivially move assignable.
We already have these traits. Admittedly we (WG21) desperately need to clean them up around the edges, but we do already have them. You (the user-programmer) just have to start using them.
Now, this does leave you with the question: "Should `is_trivially_copy_constructible_v<Poly>` ever be true?" Sadly the answer is no, because a polymorphic type can have a copy constructor that changes the vptr. I'm not talking about ARM64e stuff here — just simple inheritance.
struct Animal { virtual int f(); };
struct Cat : Animal { int f() override; };
const Animal& a = Cat();
Animal b = a;
Copying from `a` to `b` isn't a simple memcpy on
any platform. (
Godbolt.)
assert(memcmp(&a, &b, sizeof(Animal)) != 0); // invariably true
HTH,
Arthur