I had an interesting discussion with a guy smarter than me and I remained with an open question about aligned storage and trivially copyable/destructible types.
Consider the following example:
#include <type_traits>
#include <vector>
#include <cassert>
struct type {
using storage_type = std::aligned_storage_t<sizeof(void *), alignof(void *)>;
using fn_type = int(storage_type &);
template<typename T>
static int proto(storage_type &storage) {
static_assert(std::is_trivially_copyable_v<T>);
static_assert(std::is_trivially_destructible_v<T>);
return *reinterpret_cast<T *>(&storage);
}
std::aligned_storage_t<sizeof(void *), alignof(void *)> storage;
fn_type *fn;
bool weak;
};
int main() {
static_assert(std::is_trivially_copyable_v<type>);
static_assert(std::is_trivially_destructible_v<type>);
std::vector<type> vec;
type t1;
new (&t1.storage) char{'c'};
t1.fn = &type::proto<char>;
t1.weak = true;
vec.push_back(t1);
type t2;
new (&t2.storage) int{42};
t2.fn = &type::proto<int>;
t2.weak = false;
vec.push_back(t2);
vec.erase(std::remove_if(vec.begin(), vec.end(), [](const auto &t) { return t.weak; }), vec.end());
assert(vec.size() == 1);
assert(!vec[0].weak);
assert(vec[0].fn(vec[0].storage) == 42);
}
This is a simplified version of a real world case. I really hope I didn't make errors or simplified it too much.
As you can see, the idea is that there exists a type called type
(naming things is hard, you know) having three data members:
storage
that is a bunch of byte having size sizeof(void *)
fn
a pointer to a function having type int(storage_type &)
weak
an useless bool used only to introduce the exampleTo create new instances of type
(see the main
function), I put a value (either an int
or a char
) in the storage area and the right specialization of the static function template proto
in fn
.
Later on, when I want to invoke fn
and get the integer value it returns, I do something like this:
int value = type_instance.fn(type_instance.storage);
So far, so good. Despite the fact of being risky and error-prone (but this is an example, the real use case is not), this works.
Note that type
and all the types I put in the storage (int
and char
in the example) are required to be both trivially copyable and trivially destructible. This is also the core of the discussion I had.
The problem (or better, the doubt) arises when I put instances of types eg in a vector (see the main
function) and decide to remove one of them from within the array, so that some of the others are moved around to keep it packed.
More in general, I'm no longer that sure about what happens when I want to copy or move instances of type
and if it's UB or not.
My guess was that it was allowed being the types put in the storage trivially copyable and trivially destructible. On the other side, I've been told that this isn't directly allowed by the standard and it can be considered a benign UB, because almost all the compilers in fact allows you to do that (I can guarantee this, it seemes to work everywhere for some definitions of work).
So, the question is: is this allowed or UB and what can I do to work around the issue in the second case? Moreover, is C++20 going to change things for that?
This problem reduces to basically what LanguageLawyer suggested:
alignas(int) unsigned char buff1[sizeof(int)]; alignas(int) unsigned char buff2[sizeof(int)]; new (buff1) int {42}; std::memcpy(buff2, buff1, sizeof(buff1)); assert(*std::launder(reinterpret_cast<int*>(buff2)) == 42); // is it ok?
In other words - when I copy bytes around, do I also copy around "object-ness"? buff1
is certainly providing storage for an int
- when we copy those bytes, does buff2
also now provide storage for an int
?
And the answer is... no. There are exactly four ways to create an object, per [intro.object]:
An object is created by a definition, by a new-expression ([expr.new]), when implicitly changing the active member of a union, or when a temporary object is created ([conv.rval], [class.temporary]).
None of those things happened here, so we don't have an object in buff2
of any kind (outside of just the normal array of unsigned char
), hence behavior is undefined. Simply put, memcpy
does not create objects.
In the original example, it's only the 3rd line that requires that implicit object creation:
assert(vec.size() == 1); // ok
assert(!vec[0].weak); // ok
assert(vec[0].fn(vec[0].storage) == 42); // UB
This is why P0593 exists and has a special section for memmove
/memcpy
:
A call to memmove behaves as if it
- copies the source storage to a temporary area
- implicitly creates objects in the destination storage, and then
- copies the temporary storage to the destination storage.
This permits memmove to preserve the types of trivially-copyable objects, or to be used to reinterpret a byte representation of one object as that of another object.
This is what you need here - that implicit object creation step is currently missing from C++ today.
That said, you can more or less rely on this "doing the right thing" given the simply enormous body of C++ code that exists today relies on this code to "just work."
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