I would like to know what type introspection I can do to detect types that assignable by simply raw memory copy?
For example, as far I understand, built-in types tuples of built-in types and tuple of such tuples, would fall in this category. The motivation is that I want to transport raw bytes if possible.
T t1(...); // not necessarely default constructible
T t2(...);
t1 = t2; // should be equivalent to std::memcpy(&t1, &t2, sizeof(T));
// t1 is now an (independent) copy of the value of t2, for example each can go out of scope independently
What type_trait
or combination of type_traits
could tell at compile time if assignment can be (in principle) replaced by memcpy
?
I tried what would work for the types I would guess should fullfil this condition and to my surprise the only one that fit the behavior is not std::is_trivially_assignable
but std::trivially_destructible
.
It makes sense to some level, but I am confused why some other options do not work with the expected cases.
I understand that there may not be a bullet proof method because one can always write a class that effectively is memcopyable, that cannot be "detected" as memcopyable, but I am looking for one that works for the simple intuitive cases.
#include<type_traits>
template<class T> using trait =
std::is_trivially_destructible
// std::is_trivial
// std::is_trivially_copy_assignable
// std::is_trivially_copyable // // std::tuple<double, double> is not trivially copyable!!!
// std::is_trivially_default_constructible
// std::is_trivially_default_constructible
// std::is_trivially_constructible
// std::is_pod // std::tuple<double, double> is not pod!!!
// std::is_standard_layout
// std::is_aggregate
// std::has_unique_object_representations
<T>
;
int main(){
static_assert((trait<double>{}), "");
static_assert((trait<std::tuple<double, double>>{}), "");
static_assert((not trait<std::tuple<double, std::vector<double>>>{}), "");
static_assert((not trait<std::vector<double>>{}), "");
}
Of course my conviction that tuple should be memcopyable is not based on the standard but based on common sense and practice. That is, because this is generally ok:
std::tuple<double, std::tuple<char, int> > t1 = {5.1, {'c', 8}};
std::tuple<double, std::tuple<char, int> > t2;
t2 = t1;
std::tuple<double, std::tuple<char, int> > t3;
std::memcpy(&t3, &t1, sizeof(t1));
assert(t3 == t2);
As a proof of principle, I implemented this. I added a couple of conditions related to the size to avoid some possible misleading specialization of .std::tuple
template<class T>
struct is_memcopyable
: std::integral_constant<bool, std::is_trivially_copyable<T>{}>{};
template<class T, class... Ts>
struct is_memcopyable<std::tuple<T, Ts...>> :
std::integral_constant<bool,
is_memcopyable<T>{} and is_memcopyable<std::tuple<Ts...>>{}
>
{};
template<class T1, class T2>
struct is_memcopyable<std::pair<T1, T2>> :
std::integral_constant<bool,
is_memcopyable<T1>{} and is_memcopyable<T2>{}
>
{};
This is a very limited workaround because a class like:
struct A{ std::tuple<double, double> t; };
will still unfortunately be reported as non trivially copyable and non memcopyable.
The correct test is in fact std::is_trivially_copyable
, which allows use of memcpy
for both making a new object and modifying an existing one.
Although you may be surprised that these return false for types where your intuition tells you that memcpy
ought to be ok, they are not lying; the Standard indeed makes memcpy
undefined behavior in these cases.
In the particular case of std::pair
, we can get some insight into what goes wrong:
int main()
{
typedef std::pair<double,double> P;
std::cout << "\nTC: " << std::is_trivially_copyable<P>::value;
std::cout << "\nTCC: " << std::is_trivially_copy_constructible<P>::value;
std::cout << "\nTCv: " << std::is_trivially_constructible<P, const P&>::value;
std::cout << "\n CC: " << std::is_copy_constructible<P>::value;
std::cout << "\n MC: " << std::is_move_constructible<P>::value;
std::cout << "\nTCA: " << std::is_trivially_copy_assignable<P>::value;
std::cout << "\nTCvA:" << std::is_trivially_assignable<P, const P&>::value;
std::cout << "\n CA: " << std::is_copy_assignable<P>::value;
std::cout << "\n MA: " << std::is_move_assignable<P>::value;
std::cout << "\nTD: " << std::is_trivially_destructible<P>::value;
}
TC: 0 TCC: 1 TCv: 1 CC: 1 MC: 1 TCA: 0 TCvA:0 CA: 1 MA: 1 TD: 1
Evidently it isn't trivially copy assignable.1
The pair
assignment operator is user-defined, so not trivial.
1I think that clang, gcc, and msvc are all wrong here, actually, but if it satisfied std::_is_trivially_copy_assignable
it wouldn't help, because TriviallyCopyable requires that the copy constructor, if not deleted, is trivial, and not the TriviallyCopyAssignable trait. Yeah, they're different.
A copy/move assignment operator for class X is trivial if it is not user-provided and...
vs
is_assignable_v<T, const T&>
is true and the assignment, as defined byis_assignable
, is known to call no operation that is not trivial.
The operations called by pair<double, double>
's copy assignment operator are the assignments of individual doubles, which are trivial.
Unfortunately, the definition of trivially copyable relies on the first, which pair
fails.
A trivially copyable class is a class:
- where each copy constructor, move constructor, copy assignment operator, and move assignment operator is either deleted or trivial,
- that has at least one non-deleted copy constructor, move constructor, copy assignment operator, or move assignment operator, and
- that has a trivial, non-deleted destructor.
This is only a partial answer to your question:
Type traits don't necessarily mean what their name says literally.
Specifically, let's take std::is_trivially_copyable
. You were - rightly - surprised that a tuple of two double's is not trivially copyable. How could that be?!
Well, the trait definition says:
If
T
is aTriviallyCopyable
type, provides the member constantvalue
equaltrue
. For any other type,value
isfalse
.
and the TriviallyCopyable
concept has the following requirement in its definition:
- Every copy constructor is trivial or deleted
- Every move constructor is trivial or deleted
- Every copy assignment operator is trivial or deleted
- Every move assignment operator is trivial or deleted
- At least one copy constructor, move constructor, copy assignment operator, or move assignment operator is non-deleted
- Trivial non-deleted destructor
Not quite what you would expect, right?
With all in mind, it's not necessarily the case that any of the standard library traits would combine to fit the exact requirements of "constructible by memcpy()
'ing".
To try and answer your question: std::memcpy()
does not have any direct requirements but it does have these stipulations:
- Copies count bytes from the object pointed to by src to the object pointed to by dest. Both objects are reinterpreted as arrays of unsigned char.
Now to have the qualifications that an object is Trivially Copyable the following conditions or requirements must be met:
- Every copy constructor is trivial or deleted
This implies that the class has no virtual functions or virtual base classes.
Scalar types and arrays of TriviallyCopyable objects are TriviallyCopyable as well, as well as the const-qualified (but not volatile-qualified) versions of such types.
Which leads us to std::is_trivially_copyable
If T is a TriviallyCopyable type, provides the member constant value equal true. For any other type, value is false.
The only trivially copyable types are scalar types, trivially copyable classes, and arrays of such types/classes (possibly const-qualified, but not volatile-qualified).
The behavior is undefined if std::remove_all_extents_t is an incomplete type and not (possibly cv-qualified) void.
with this nice feature since c++17:
Helper variable template
template< class T >
inline constexpr bool is_trivially_copyable_v = is_trivially_copyable<T>::value;
And you would like to try and use a type_trait
to use std::tuple<>
with std::memcpy()
.
But we need to ask ourselves if std::tuple
is Trivially Copyable
and why?
We can see the answer to that here: Stack-Q/A: std::tuple Trivially Copyable? and according to that answer; it is not because the standard does not require the copy/move assignment operators to be trivial.
So the answer that I would think that is valid would be this: No std::tuple
is not Trivially Copyable but std::memcpy()
doesn't require it to be but only states that if it isn't; it is UB. So can you use std::tuple
with std::memcpy
? I think so, but is it safe? That can vary and can produce UB.
So what can we do from here? Take a risk? Maybe. I found something else that is related but have not found anything out about it regarding if it is Trivially Copyable. It is not a type_trait
, but it is something that might be able to be used in conjunction with std::tuple
& std::memcpy
and that is std::tuple_element
. You might be able to use this to do the memcpy, but I'm not fully sure about it. I have searched to find out more about std::tuple_element
to see if it is Trivially Copyable but haven't found much so all I can do is a test to see what Visual Studio 2017 says:
template<class... Args>
struct type_list {
template<std::size_t N>
using type = typename std::tuple_element<N, std::tuple<Args...>>::type;
};
int main() {
std::cout << std::boolalpha;
std::cout << std::is_trivially_copyable<type_list<int, float, float>>::value << '\n';
std::cout << std::is_trivially_copyable<std::tuple<int, float, float>>::value << '\n';
_getch(); // used to stop visual studio debugger from closing.
return 0;
}
Output:
true
false
So it appears if we wrap std::tuple_element
in a struct it is Trivially Copyable. Now the question is how do you integrate this with your std::tuple
data sets to use them with std::memcpy()
to be type safe
. Not sure if we can since std::tuple_element
will return the types of the elements within a tuple
.
If we even tried to wrap a tuple
in a struct as such:
template<class... Args>
struct wrapper {
std::tuple<Args...> t;
};
And we can check it by:
{
std::cout << std::is_trivially_copyable< wrapper<int, float, float> >::value << std::endl;
}
It is still false
. However we have seen were std::tuple
was already used in the first struct and the struct returned true
. This may be of some help to you, to ensure you can safely use std::memcpy
, but I can not guarantee it. It is just that the compiler seems to agree with it. So this might be the closest thing to a type_trait
that might work.
NOTE: - All the references about memcpy
, Trivially Copyable concepts
, is_trivially_copyable
, std::tuple
& std::tuple_element
were taken from cppreference and their relevant pages.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With