Reinterpret_Cast Creating a Trivially Default-Constructible Object

reinterpret_cast creating a trivially default-constructible object

There is no X object, living or otherwise, so pretending that there is one results in undefined behavior.

[intro.object]/1 spells out exhaustively when objects are created:

An object is created by a definition ([basic.def]), by a
new-expression ([expr.new]), when implicitly changing the active
member of a union ([class.union]), or when a temporary object is
created ([conv.rval], [class.temporary]).

With the adoption of P0137R1, this paragraph is the definition of the term "object".

Is there a definition of an X object? No. Is there a new-expression? No. Is there a union? No. Is there a language construct in your code that creates a temporary X object? No.

Whatever [basic.life] says about the lifetime of an object with vacuous initialization is irrelevant. For that to apply, you have to have an object in the first place. You don't.

C++11 has roughly the same paragraph, but doesn't use it as the definition of "object". Nonetheless, the interpretation is the same. The alternative interpretation - treating [basic.life] as creating an object as soon as suitable storage is obtained - means that you are creating Schrödinger's objects*, which contradicts N3337 [intro.object]/6:

Two objects that are not bit-fields may have the same address if one
is a subobject of the other, or if at least one is a base class
subobject of zero size and they are of different types; otherwise,
they shall have distinct addresses.


* Storage with the proper alignment and size for a type T is by definition storage with the proper alignment and size for every other type whose size and alignment requirements are equal to or less than those of T. Thus, that interpretation means that obtaining the storage simultaneously creates an infinite set of objects with different types in said storage, all having the same address.

Why can we not create trivially constructible objects using malloc if the trivial default constructor performs no action?

P0593R5 "Implicit creation of objects for low-level object manipulation" gives this example:

struct X { int a, b; };
X *make_x() {
X *p = (X*)malloc(sizeof(struct X));
p->a = 1;
p->b = 2;
return p;
}

and explains:

When compiled with a C++ compiler, this code has undefined behavior, because p->a attempts to write to an int subobject of an X object, and this program never created either an X object nor an int subobject.

Per [intro.object]p1 (C++17 Draft N4659),

An object is created by a definition, by a new-expression, when implicitly changing the active member of a union, or when a temporary object is created.

... and this program did none of these things.

In practice this works and the UB situation is considered more as a defect in the standard than anything else. The whole objective of the paper is to propose a way to fix that issue and similar cases without breaking other things.

Initializing an array of trivially_copyable but not default_constructible objects from bytes. Confusion in [intro.object]

What you're trying to do ultimately is create an array of some type T by memcpying bytes from elsewhere without default constructing the Ts in the array first.

Pre-C++20 cannot do this without provoking UB at some point.

The problem ultimately comes down to [intro.object]/1, which defines the ways objects get created:

An object is created by a definition, by a new-expression, when implicitly changing the active member of a union, or when a temporary object is created ([conv.rval], [class.temporary]).

If you have a pointer of type T*, but no T object has been created in that address, you can't just pretend that the pointer points to an actual T. You have to cause that T to come into being, and that requires doing one of the above operations. And the only available one for your purposes is the new-expression, which requires that the T is default constructible.

If you want to memcpy into such objects, they must exist first. So you have to create them. And for arrays of such objects, that means they need to be default constructible.

So if it is at all possible, you need a (likely defaulted) default constructor.


In C++20, certain operations can implicitly create objects (provoking "implicit object creation" or IOC). IOC only works on implicit lifetime types, which for classes:

A class S is an implicit-lifetime class if it is an aggregate or has at least one trivial eligible constructor and a trivial, non-deleted destructor.

Your class qualifies, as it has a trivial copy constructor (which is "eligible") and a trivial destructor.

If you create an array of byte-wise types (unsigned char, std::byte, or char), this is said to "implicitly create objects" in that storage. This property also applies to the memory returned by malloc and operator new. This means that if you do certain kinds of undefined behavior to pointers to that storage, the system will automatically create objects (at the point where the array was created) that would make that behavior well-defined.

So if you allocate such storage, cast a pointer to it to a T*, and then start using it as though it pointed to a T, the system will automatically create Ts in that storage, so long as it was appropriately aligned.

Therefore, your alternative A works just fine:

When you apply [index] to your casted pointer, C++ will retroactively create an array of Foo in that storage. That is, because you used the memory like an array of Foo exists there, C++20 will make an array of Foo exist there, exactly as if you had created it back at the new unsigned char statement.

However, alternative B will not work as is. You did not use new[] Foo to create the array, so you cannot use delete[] Foo to delete it. You can still use unique_ptr, but you'll have to create a deleter that explicitly calls operator delete on the pointer:

struct mem_delete
{
template<typename T>
void operator(T *ptr)
{
::operator delete[](ptr);
}
};

std::unique_ptr<Foo[], mem_delete> storage{
static_cast<Foo *>(::operator new[](dynamicSize * sizeof(Foo))) };

input.read(reinterpret_cast<char *>(storage.get()), dynamicSize * sizeof(Foo));

std::cout << storage[index].alpha << "\n";

Again, storage[index] creates an array of T as if it were created at the time the memory was allocated.

std::realloc with static_cast or placement new

Practical answer: If realloc isn't allowed for trivially copyable types (that is, all copy and move constructors and assignment operators are trivial, there is at least one copy or move constructor or assignment operator, and the destructor is trivial), this seems like a bug in the standard I can't imagine a compiler writer causing issues here.

Language lawyer answer: I don't think this is allowed by the standard for any type other than char, unsigned char, and std::byte, and I'm no longer as certain about those as I was when I commented to the question (it would be fine if there were some underlying object created by realloc, but there isn't). These functions are specified to "have the semantics specified in the C standard library", which isn't particularly helpful because C doesn't have a semantic equivalent to the C++ concept of "object". The general understanding of malloc is that it does not create a C++ object, so you need to do that yourself with placement new; I would assume the same happens for realloc.

Your proposed solution of using placement new[] on the result of realloc would also not work. Because new[] needs to keep track of the number of elements allocated (for use by delete[]), it is allowed to add some overhead space, and then return a pointer later than the one it received so the first part is the overhead. Although this isn't necessary with placement new[], it is still allowed (which makes me think there is no valid way to use placement new[] at all because you don't know how much overhead will be requested, so can't be sure you have enough space for it).

Even if you ensured you only used non-array placement new (I can think of a couple workarounds where realloc still makes sense), it would indeed create a valid object there (for types with trivial default constructors, which are at least allowed to do no work when default-initialized), but it might have indeterminate value. Right now it seems to me that it probably works but the standard is somewhat ambiguous, but that is debatable and I may be wrong.

What is happening in this code? reinterpret_castint*(buf)

Does it put 6 integers and then 5 floats into the buffer?

Yes.

It's also strange that they set size to 11 instead of 1024*sizeof(char)

They don't want to write the entire buffer. Thy want to write just the ints and floats that were written to the buffer.

FWIW, that is poorly written code. It assumes that sizeof(int) and sizeof(float) are both equal to 4. A more portable method would use:

int size = 6*sizeof(int) + 5*sizeof(float);

Caution

Even though the posted code might work under some, perhaps most, circumstances, use of

int* pInt = reinterpret_cast<int*>(buf);
*pInt = 5;

is cause for undefined behavior by the standard. It violates the strict aliasing rule. There wasn't an int to begin with at that location.

Had you used:

int array[5] = {};
char* cp = reinterpret_cast<char*>(array);

// ...

int* iptr = reinterpret_cast<int*>(cp);
*iptr = 10;

there would be no problem since cp points to a place where an int was there to begin with.

For your use case, it will be better to use:

char buf[1024];
int intArray[] = {5, 2, 3, 4, 5, 6};
std::memcpy(buff, intArray, sizeof(intArray));

float floatArray = {111, 222, 333, 444, 555};
std::memcpy(buff+sizeof(intArray), floatArray, sizeof(floatArray));

int n;
int size = sizeof(intArray) + sizeof(floatArray);
n = write(buf, size);

Further reading:

  1. reinterpret_cast creating a trivially default-constructible object
  2. Unions and type-punning

Managing trivial types

  1. No, you can't. There is no object of type T in that storage, and accessing the storage as if there was is undefined. See also T.C.'s answer here.

    Just to clarify on the wording in [basic.life]/1, which says that objects with vacuous initialization are alive from the storage allocation onward: that wording obviously refers to an object's initialization. There is no object whose initialization is vacuous when allocating raw storage with operator new or malloc, hence we cannot consider "it" alive, because "it" does not exist. In fact, only objects created by a definition with vacuous initialization can be accessed after storage has been allocated but before the vacuous initialization occurs (i.e. their definition is encountered).

  2. Omitting destructor calls never per se leads to undefined behavior. However, it's pointless to attempt any optimizations in this area in e.g. templates, since a trivial destructor is just optimized away.

  3. Right now, the requirement is being trivially copyable, and the types have to match. However, this may be too strict. Dos Reis's N3751 at least proposes distinct types to work as well, and I could imagine this rule being extended to trivial copy assignment across one type in the future.

    However, what you've specifically shown does not make a lot of sense (not least because you're asking for assignment to a scalar xvalue, which is ill-formed), since trivial assignment can hold between types whose assignment is not actually "trivial", that is, has the same semantics as memcpy. E.g. is_trivially_assignable<int&, double> does not imply that one can be "assigned" to the other by copying the object representation.

Object access using reinterpret_cast for struct {double, int}-like object

As Barry states better here, 1&3 are UB. The short version: none of those pieces of code contain any of the syntax needed to create an object. And you can't access the value of an object that isn't there.

So, do 2 and 4 work?

#2 works if and only if alignof(double) >= alignof(int). But it only works in the sense that it create a double followed by an int. It does not in any way "emulate" that nameless struct. The struct could have any arbitrary amount of padding, while in this case, the int will immediately follow the double.

#4 does not work, strictly speaking. buff does not actually point to the newly created double. As such, pointer arithmetic cannot be used to get the byte after that object. So doing pointer arithmetic yields undefined behavior.

Now, we are talking about C++ strictly speaking. In all likelihood, every compiler will execute all four of these (with the above caveat about alignment).

How evil would it be to use type punning between trivially copyable structs?

C++ abstracts the concept of a lifetime, even for pods with no constructors, C++ defines in specific terms when lifetime starts and ends of an object, that's why you can't just reinterpret bytes from a memory even if you know their layout match. It is undefined behavior because that is not start of lifetime of the object.

In practice, this is the kind of of UB that people still use though, because there's no equivalent non UB option.

std::start_lifetime_as<T> and new (p) std::byte[n] (formerly std::bless) would be the perfect remedy for this (http://wg21.link/p0593) but sadly not for now.

trivially default constructible std::optional and std::variant

Your answer is correct: you cannot. The specification requires that its "initialized flag" is set to false upon default construction.



Related Topics



Leave a reply



Submit