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
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 cv T
", the result is static_cast<
cv T*>(static_cast<
cv void*>(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
", where T
is an object type, can be converted to a prvalue of type "pointer to cv void
". 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 cv2 T
", where T
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 address A
of a byte in memory and A
does not satisfy the alignment requirement of T
, 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 type T
(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.