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

拈花ヽ惹草 提交于 2019-11-28 11:59:24

There is no living C object, so pretending that there is one results in undefined behavior.

P0137R1, adopted at the committee's Oulu meeting, makes this clear by defining object as follows ([intro.object]/1):

An object is created by a definition ([basic.def]), by a new-expression ([expr.new]), when implicitly changing the active member of a union ([class.union]), or when a temporary object is created ([conv.rval], [class.temporary]).

reinterpret_cast<C*>(malloc(sizeof(C))) is none of these.

Also see this std-proposals thread, with a very similar example from Richard Smith (with a typo fixed):

struct TrivialThing { int a, b, c; };
TrivialThing *p = reinterpret_cast<TrivialThing*>(malloc(sizeof(TrivialThing))); 
p->a = 0; // UB, no object of type TrivialThing here

The [basic.life]/1 quote applies only when an object is created in the first place. Note that "trivial" or "vacuous" (after the terminology change done by CWG1751) initialization, as that term is used in [basic.life]/1, is a property of an object, not a type, so "there is an object because its initialization is vacuous/trivial" is backwards.

I think the code is ok, as long as the type has a trivial constructor, as yours. Using the object cast from malloc without calling the placement new is just using the object before calling its constructor. From C++ standard 12.7 [class.dctor]:

For an object with a non-trivial constructor, referring to any non-static member or base class of the object before the constructor begins execution results in undefined behavior.

Since the exception proves the rule, referrint to a non-static member of an object with a trivial constructor before the constructor begins execution is not UB.

Further down in the same paragraphs there is this example:

extern X xobj;
int* p = &xobj.i;
X xobj;

This code is labelled as UB when X is non-trivial, but as not UB when X is trivial.

For the most part, circumventing the constructor generally results in undefined behavior.

There are some, arguably, corner cases for plain old data types, but you don't win anything avoiding them in the first place anyway, the constructor is trivial. Is the code as simple as presented?

[basic.life]/1

The lifetime of an object or reference is a runtime property of the object or reference. An object is said to have non-vacuous initialization if it is of a class or aggregate type and it or one of its subobjects is initialized by a constructor other than a trivial default constructor. [ Note: initialization by a trivial copy/move constructor is non-vacuous initialization. — end note ] The lifetime of an object of type T begins when:

  • storage with the proper alignment and size for type T is obtained, and
  • if the object has non-vacuous initialization, its initialization is complete.

The lifetime of an object of type T ends when:

  • if T is a class type with a non-trivial destructor ([class.dtor]), the destructor call starts, or
  • the storage which the object occupies is reused or released.

Aside from code being harder to read and reason about, you will either not win anything, or land up with undefined behavior. Just use the constructor, it is idiomatic C++.

This particular code is fine, because C is a POD. As long as C is a POD, it can be initialized that way as well.

Your code is equivalent to this:

struct C
{
   int *x;
};

C* c = (C*)malloc(sizeof(C)); 
c->x = NULL;

Does it not look like familiar? It is all good. There is no problem with this code.

While you can initialize all explicit members that way, you cannot initialize everything a class may contain:

  1. references cannot be set outside an initializer list

  2. vtable pointers cannot be manipulated by code at all

That is, the moment that you have a single virtual member, or virtual base class, or reference member, there is no way to correctly initialize your object except by calling its constructor.

I think it shouldn't be UB. You make your pointer point to some raw memory and are treating its data in a particular way, there's nothing bad here.

If the constructor of this class does something (initializes variables, etc), you'll end up with, again, a pointer to raw, uninitialized object, using which without knowing what the (default) constructor was supposed to be doing (and repeating its behavior) will be UB.

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!