I recently saw a class like this that was used to construct objects "on-demand" without having to use dynamic memory allocation for various reasons.
#include <cassert>
template<typename T>
class StaticObject
{
public:
StaticObject() : constructed_(false)
{
}
~StaticObject()
{
if (constructed_)
((T*)object_)->~T();
}
void construct()
{
assert(!constructed_);
new ((T*)object_) T;
constructed_ = true;
}
T& operator*()
{
assert(constructed_);
return *((T*)object_);
}
const T& operator*() const
{
assert(constructed_);
return *((T*)object_);
}
private:
bool constructed_;
alignas(alignof(T)) char object_[sizeof(T)];
};
Is this code, namely the casting of a properly aligned char array to an object pointer, considered undefined behavior by the C++14 standard or is it completely fine?
This program technically has undefined behavior, although it's likely to work on most implementations. The issue is that a cast from char*
to T*
is not guaranteed to result in a valid pointer to the T
object created by placement new, even though the char*
pointer represents the address of the first byte used for storage for the T
object.
[basic.compound]/3:
Pointers to layout-compatible types shall have the same value representation and alignment requirements ([basic.align]).
In general, T
will not be layout-compatible with char
or with alignas(T) char[sizeof(T)]
, so there's no requirement that a pointer T*
has the same value representation as a pointer char*
or void*
.
[basic.compound]/4:
Two objects a and b are pointer-interconvertible if:
they are the same object, or
one is a union object and the other is a non-static data member of that object ([class.union]), or
one is a standard-layout class object and the other is the first non-static data member of that object, or, if the object has no non-static data members, any base class subobject of that object ([class.mem]), or
there exists an object c such that a and c are pointer-interconvertible, and c and b are pointer-interconvertible.
If two objects are pointer-interconvertible, then they have the same address, and it is possible to obtain a pointer to one from a pointer to the other via a
reinterpret_cast
. [ Note: An array object and its first element are not pointer-interconvertible, even though they have the same address. — end note ]
[Aside: DR 2287 changed "standard-layout union" to "union" in the second bullet after the publication of C++17. But that doesn't affect this program.]
The T
object created by the placement new is not pointer-interconvertible with object_
or with object_[0]
. And the note hints that this might be a problem for casts...
For the C-style cast ((T*)object_)
, we need to see [expr.cast]/4:
The conversions performed by
a
const_cast
,a
static_cast
,a
static_cast
followed by aconst_cast
,a
reinterpret_cast
, ora
reinterpret_cast
followed by aconst_cast
can be performed using the cast notation of explicit type conversion....
If a conversion can be interpreted in more than one of the ways listed above, the interpretation that appears first in the list is used, even if a cast resulting from that interpretation is ill-formed.
Unless T
is char
or cv-qualified char
, this will effectively be a reinterpret_cast
, so next we look at [expr.reinterpret.cast]/7:
An object pointer can be explicitly converted to an object pointer of a different type. When a prvalue
v
of object pointer type is converted to the object pointer type "pointer to cvT
", the result isstatic_cast<
cvT*>(static_cast<
cvvoid*>(v))
.
So first we have a static_cast
from char*
to void*
, which does the standard conversion described in [conv.ptr]/2:
A prvalue of type "pointer to cv
T
", whereT
is an object type, can be converted to a prvalue of type "pointer to cvvoid
". The pointer value ([basic.compound]) is unchanged by this conversion.
This is followed by a static_cast
from void*
to T*
, described in [expr.static.cast]/13:
A prvalue of type "pointer to cv1
void
" can be converted to a prvalue of type "pointer to cv2T
", whereT
is an object type and cv2 is the same cv-qualification as, or greater cv-qualification than, cv1. If the original pointer value represents the addressA
of a byte in memory andA
does not satisfy the alignment requirement ofT
, then the resulting pointer value is unspecified. Otherwise, if the original pointer value points to an object a, and there is an object b of typeT
(ignoring cv-qualification) that is pointer-interconvertible with a, the result is a pointer to b. Otherwise, the pointer value is unchanged by the conversion.
As already noted, the object of type T
is not pointer-interconvertible with object_[0]
, so that sentence does not apply, and there's no guarantee that the result T*
points at the T
object! We're left with the sentence saying "the pointer value is unchanged", but this might not be the result we want if the value representations for char*
and T*
pointers are too different.
A Standard-compliant version of this class could be implemented using a union
:
template<typename T>
class StaticObject
{
public:
StaticObject() : constructed_(false), dummy_(0) {}
~StaticObject()
{
if (constructed_)
object_.~T();
}
StaticObject(const StaticObject&) = delete; // or implement
StaticObject& operator=(const StaticObject&) = delete; // or implement
void construct()
{
assert(!constructed_);
new(&object_) T;
constructed_ = true;
}
T& operator*()
{
assert(constructed_);
return object_;
}
const T& operator*() const
{
assert(constructed_);
return object_;
}
private:
bool constructed_;
union {
unsigned char dummy_;
T object_;
}
};
Or even better, since this class is essentially attempting to implement an optional
, just use std::optional
if you have it or boost::optional
if you don't.
Casting a char array to an object pointer - is this UB?
Casting one pointer (the array decays to a pointer) to another pointer that is not in same inheritance hierarchy using a C-style cast performs a reinterpret cast. A reinterpret cast itself never has UB.
However, indirecting a converted pointer can have UB if an object of appropriate type has not been constructed into that address. In this case, an object has been constructed in the character array, so the indirection has well defined behaviour. Edit: The indirection would be UB free, if it weren't for the strict aliasing rules; see ascheplers answer for details. aschepler shows a C++14 conforming solution. In C++17, your code can be corrected with following changes:
void construct()
{
assert(!constructed_);
new (object_) T; // removed cast
constructed_ = true;
}
T& operator*()
{
assert(constructed_);
return *(std::launder((T*)object_));
}
To construct an object into an array of another type, three requirements must be met to avoid UB: The other type must be allowed to alias the object type (char
, unsigned char
and std::byte
satisfy this requirement for all object types), the address must be aligned to the memory boundary as required by the object type and none of the memory must overlap with the lifetime of another object (ignoring the underlying objects of the array which are allowed to alias the overlaid object). All of those requirements are satisfied by your program.
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