Introduction
I was recently reading the C++ standard, for what it means for a type T to be trivially copyable. This article is kind of inspired by Jonathan Muller’s blog post, Trivially copyable is not the same as trivially copy constructible. Intuitively, a trivially copyable object can be treated as a block of bytes and copied using primitive memory operations(std::memcpy and std::memmove). The C++ standard defines:
[class.prop/11.2] A trivially copyable class is a class:
that has at least one of the special members : copy constructor, move constructor, copy assignment operator, or move assignment operator.
where each copy constructor, move constructor, copy assignment operator, and move assignment operator is trivial, and
that has a trivial, non-deleted destructor.
In addition, every base class and every member variable of the class must be recursively trivially copyable.
#include <iostream>
#include <type_traits>
struct Point2D{
double x,y;
~Point2D() = default;
Point2D() = default;
Point2D(const Point2D&) = default;
};
static_assert(std::is_trivially_copyable_v<Point2D>);
int main(){}The library-writer’s intuition is that, when a type is trivially copyable, all of its rule-of-five operations can be lowered to simple memcpys.
The compiler-writer’s intuition is that all types start out as TC, but then get made non-TC in various ways. If any of T’s rule-of-five members are user-defined, we mark T as non-TC. All of T’s member variables should be recursively TC. If it has a sub-object of non-TC type, we mark it non-TC. While this is easy to implement, it doesn’t work perfectly, because the set of types actually considered TC by C++ includes some types that violate this paradigm. For example, surprisingly, it is easy to construct a TC type, that has a non-TC data-member.
What is an eligible copy/move ctor or assign operator?
There are two important things to keep in mind. In C++, a copy constructor, copy assignment operator is never a template.
#include <print>
#include <type_traits>
struct Widget{
Widget() = default;
~Widget() = default;
Widget(const volatile Widget&){
std::print("Launching a rocket!"); // non-trivial
}
template<typename T>
Widget(const T& w); // Not a copy constructor
};
static_assert(!std::is_trivially_copyable_v<Widget>);
int main(){}Surprising behaviors
Consider the below code snippet. Widget is non-TC. WidgetMarketPlace is an aggregate of 2 Widgets. Currently, there is an implementation divergence - clang + gcc correctly reports WidgetMarketPlace as non-TC, whereas MSVC says true.
David Vandevoorde made a comment on this issue - it is unclear why a Widget cannot be memcpyed, but Widgets can be memcpyed when embedded in a WidgetMarketPlace object. Arthur argues, that to the library writer’s intuition, this makes perfect sense : all provided operations in WidgetMarketPlace are equivalent to doing a memcpy.
#include <print>
#include <type_traits>
struct Widget{
Widget() = default;
~Widget() = default;
Widget(const Widget&){
std::print("Launching a rocket!");
}
Widget(Widget&&) = default;
Widget& operator=(Widget&&) = default;
};
struct WidgetsMarketPlace{
Widget w1,w2;
WidgetsMarketPlace() = default;
~WidgetsMarketPlace() = default;
WidgetsMarketPlace(WidgetsMarketPlace&&) = default;
};
static_assert(!std::is_trivially_copyable_v<Widget>);
static_assert(std::is_trivially_copyable_v<WidgetsMarketPlace>);
int main(){}There are reasons why, is_trivially_fooable is not a settled issue. There are multiple papers still battling over the subject.
Trivially copyable is not the same as trivially copy constructible
A type T is said to be trivially copy constructible, if its copy constructor is trivial (implicitly defined or =defaulted) and each of its data-members are recursively trivially copy constructible. The current implementation of 𝚜𝚝𝚍::𝚒𝚜_𝚝𝚛𝚒𝚟𝚒𝚊𝚕𝚕𝚢_𝚌𝚘𝚙𝚢𝚊𝚋𝚕𝚎_𝚟<𝚃> in libc++ merely calls the compiler builtin __𝚒𝚜_𝚝𝚛𝚒𝚟𝚒𝚊𝚕𝚕𝚢_𝚌𝚘𝚙𝚢𝚊𝚋𝚕𝚎(𝚃) and clang’s implementation of __𝚝𝚛𝚒𝚟𝚒𝚊𝚕𝚕𝚢_𝚌𝚘𝚙𝚢𝚊𝚋𝚕𝚎(𝚃) in the AST, conforms to the C++ standard. But, there is a key difference between the two categories of traits:
𝚜𝚝𝚍::𝚒𝚜_𝚝𝚛𝚒𝚟𝚒𝚊𝚕𝚕𝚢_𝚌𝚘𝚙𝚢_𝚌𝚘𝚗𝚜𝚝𝚛𝚞𝚌𝚝𝚒𝚋𝚕𝚎_𝚟<𝚃>performs overload resolution equivalent to writing𝚃 𝚊 = 𝚋and determine whether the called special member function is indeed trivial.𝚜𝚝𝚍::𝚒𝚜_𝚝𝚛𝚒𝚟𝚒𝚊𝚕𝚕𝚢_𝚌𝚘𝚙𝚢𝚊𝚋𝚕𝚎_𝚟<𝚃>simply looks at whether𝚃(𝚜𝚝𝚍::𝚍𝚎𝚌𝚕𝚟𝚊𝚕<𝚌𝚘𝚗𝚜𝚝 𝚃&()>)is well-formed.
This makes them fundamentally incompatible.
Consider the below code snippet -
#include <iostream>
#include <type_traits>
struct Weird{
~Weird() = default;
Weird(const volatile Weird&) = delete;
template<int i = 0> // not a copy constructor
Weird(const Weird&){
std::cout << "template method";
}
Weird(Weird&&) = default;
};
static_assert(std::is_copy_constructible_v<Weird>);
static_assert(!std::is_trivially_copy_constructible_v<Weird>);
static_assert(std::is_trivially_copyable_v<Weird>);
int main(){}References
- Trivially copyable does not mean trivially copy constructible, Jonathan Muller
- What trivially fooable should mean P3279R0, Arthur O Dwyer