Is there a (semantic) difference between the return value of placement new and the casted value of its operand?

Only a can safely be used to directly access the Foo object created by the placement new-expression (which we'll call x for ease of reference). Using b requires std::launder.

The value of a is specified in [expr.new]/1:

If the entity is a non-array object, the result of the new-expression is a pointer to the object created.

The value of a is therefore "pointer to x". This pointer, of course, can safely be used to access x.

reinterpret_cast<Foo*>(buffer) applies the array-to-pointer conversion to buffer (see [expr.reinterpret.cast]/1). The resulting value after the conversion is applied is "pointer to the first element of buffer". This is a reinterpret_cast of an object pointer to an object pointer of a different type, and is defined as equivalent to static_cast<Foo*>(static_cast<void*>(buffer)) by [expr.reinterpret.cast]/7.

The inner cast to void* is actually an implicit conversion. Per [conv.ptr]/2,

The pointer value is unchanged by this conversion.

Therefore the inner cast yields a void* with the value "pointer to the first element of buffer".

The outer cast is governed by [expr.static.cast]/13, which I've lightly reformatted into bullet points:

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.

Assuming that buffer is suitably aligned (you'd be in trouble well before this point if it's not), the first bullet is inapplicable. The second bullet is likewise inapplicable as there's no pointer-interconvertiblity here. It follows that we hit the third bullet - "the pointer value is unchanged by the conversion" and remains "pointer to the first element of buffer".

Thus, b does not point to the Foo object x; it points instead to the first char element of buffer, even though its type is Foo*. It therefore cannot be used to access x; attempting to do so yields undefined behavior (for the non-static data member case, by omission from [expr.ref]; for the non-static member function case, by [class.mfct.non-static]/2).

To recover a pointer to x from b, std::launder can be used:

b = std::launder(b); // value of b is now "pointer to x"
                     // and can be used to access x

Access through a is legal while b is not. From [basic.compound]

Two objects a and b are pointer-interconvertible if:

  • they are the same object, or

  • one is a standard-layout union object and the other is a non-static data member of that object, 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, the first 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 ]

They are not the same object, not unions and not subobjects to each other, therefore not pointer-interconvertible.

Note [expr.reinterpret.cast] only guarantees reinterpret_cast<char*>(b) == buffer.

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)). [ Note: Converting a prvalue of type “pointer to T1” to the type “pointer to T2” (where T1 and T2 are object types and where the alignment requirements of T2 are no stricter than those of T1) and back to its original type yields the original pointer value.  — end note ]