Casting a char array to an object pointer - is this UB?

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 a const_cast,

  • a reinterpret_cast, or

  • a reinterpret_cast followed by a const_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 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.


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.