Is circumventing a class’ constructor legal or does it result in undefined behaviour?

It is legal now, and retroactively since C++98!

Indeed the C++ specification wording till C++20 was defining an object as (e.g. C++17 wording, [intro.object]):

The constructs in a C++ program create, destroy, refer to, access, and
manipulate objects. An object is created by a definition (6.1), by a
new-expression (8.5.2.4), when implicitly changing the active member
of a union (12.3), or when a temporary object is created (7.4, 15.2).

The possibility of creating an object using malloc allocation was not mentioned. Making it a de-facto undefined behavior.

It was then viewed as a problem, and this issue was addressed later by https://wg21.link/P0593R6 and accepted as a DR against all C++ versions since C++98 inclusive, then added into the C++20 spec, with the new wording:

[intro.object]

  1. The constructs in a C++ program create, destroy, refer to, access, and manipulate objects. An object is created by a definition, by a new-expression, by an operation that implicitly creates objects (see below)

  1. Further, after implicitly creating objects within a specified region of
    storage, some operations are described as producing a pointer to a
    suitable created object. These operations select one of the
    implicitly-created objects whose address is the address of the start
    of the region of storage, and produce a pointer value that points to
    that object, if that value would result in the program having defined
    behavior. If no such pointer value would give the program defined
    behavior, the behavior of the program is undefined. If multiple such
    pointer values would give the program defined behavior, it is
    unspecified which such pointer value is produced.

The example given in C++20 spec is:

#include <cstdlib>
struct X { int a, b; };
X *make_x() {
   // The call to std​::​malloc implicitly creates an object of type X
   // and its subobjects a and b, and returns a pointer to that X object
   // (or an object that is pointer-interconvertible ([basic.compound]) with it), 
   // in order to give the subsequent class member access operations   
   // defined behavior. 
   X *p = (X*)std::malloc(sizeof(struct X));
   p->a = 1;   
   p->b = 2;
   return p;
}

Leave a Comment