Home > Back-end >  Is it possible for implicit object creation to not create objects in certain situations?
Is it possible for implicit object creation to not create objects in certain situations?

Time:08-21

According to [intro.object]/10:

Some operations are described as implicitly creating objects within a specified region of storage. For each operation that is specified as implicitly creating objects, that operation implicitly creates and starts the lifetime of zero or more objects of implicit-lifetime types ([basic.types.general]) in its specified region of storage if doing so would result in the program having defined behavior. If no such set of objects would give the program defined behavior, the behavior of the program is undefined. If multiple such sets of objects would give the program defined behavior, it is unspecified which such set of objects is created.

it can choose not to create objects if that would make the program legal.

Consider the following code (from this question):

void* p = operator new(1);
new (p) char;
delete static_cast<char*>(p);

operator new implicitly creates objects, according to [intro.object]/13:

Any implicit or explicit invocation of a function named operator new or operator new[] implicitly creates objects in the returned region of storage and returns a pointer to a suitable created object.

It also follows [intro.object]/10, so consider two options:

  1. Implicitly creates a char object. See this answer.
  2. Does not create a char object. First, p points to uninitialized memory allocated by operator new. Then, explicitly create a char object on it by placement new. Finally, it is used on delete-expression.

The question is whether option 2 is legal, that is, whether p automatically points to the object created by placement new.

The standard rules at [basic.life]/8:

If, after the lifetime of an object has ended and before the storage which the object occupied is reused or released, a new object is created at the storage location which the original object occupied, a pointer that pointed to the original object, a reference that referred to the original object, or the name of the original object will automatically refer to the new object and, once the lifetime of the new object has started, can be used to manipulate the new object, if the original object is transparently replaceable (see below) by the new object.

But no object occupies the memory pointed to by p. Therefore, no replacement takes place, [basic.life]/8 doesn't apply in this case.

I didn't find a stipulation in the standard. So, does the standard allow option 2?

CodePudding user response:

it can choose not to create objects if that would make the program legal.

Um, no; it cannot. The literal text you quoted says "that operation implicitly creates and starts the lifetime of ... if doing so would result in the program having defined behavior." There is no conditional here, no choice about it. So if the program would otherwise have undefined behavior, and creating a particular object type would give it defined behavior, IOC will do that.

It is not optional; if it is available, it must happen.

Option 1 is not only possible, it is required. It is required because option 2 is undefined behavior.

Let's take it step by step.

void* p = operator new(1);

If IOC does not happen, then p does not point to an object. It merely points to memory. That's just what operator new does.

new (p) char;

This creates a char object in the storage pointed to by p. However, this does not cause p to point to that object. I can't cite a portion of the standard because the standard doesn't say what doesn't happen. [expr.new] explains how new expressions work, and nothing there, or in the definition of the placement-new functions, says that it changes the nature of the argument that it is given.

Therefore, p continues to point at storage and not an object in that storage.

delete static_cast<char*>(p);

This is where UB happens. [expr.delete]/11 tells us:

For a single-object delete expression, the deleted object is the object denoted by the operand if its static type does not have a virtual destructor, and its most-derived object otherwise.

Well, p does not denote an object at all. It just points to storage. static_cast<char*>(p) does not have the power to reach into that storage and get a pointer to an object in that storage. At least, not unless the object is pointer-interconvertible with the object pointed to by p. But since p doesn't point to an object at all at this point... the cast cannot change this.

Therefore, we have achieved undefined behavior.

However, this is where IOC gets triggered. There is a way for IOC to fix this. If operator new(1) performed implicit object creation, and that object would be a char, then this expression would return a pointer to that char. This is what the "pointer to a suitable created object" wording you cited means.

In this case, p points to an actual object of type char.

Then we create a new char, which overlays the original char. Because of that, [basic.life]/8 is triggered. This defines a concept called "transparently replaceable". The new object transparently replaces the old one, and therefore, pointers/references to the old object transparently become pointers/references to the new one:

a pointer that pointed to the original object, a reference that referred to the original object, or the name of the original object will automatically refer to the new object and, once the lifetime of the new object has started, can be used to manipulate the new object

Therefore, from this point onward, p points to the new char.

So the static_cast will now return a char* that points to a char, which delete can delete with well-defined behavior. Since the code otherwise would have UB and IOC can fix that UB... it must do so.


Now, if your example had been:

void* p = operator new(1);
auto p2 = new (p) char;
delete p2;

Does an object get created in p? The answer is... it doesn't matter. Because you never use p in a way that requires it to point to a created object, this program has well-defined behavior either way. So the question is irrelevant.

CodePudding user response:

In the case where the observable effect of both options is the same, both are permitted because of the “as-if rule”:

Allows any and all code transformations that do not change the observable behavior of the program.

The as-if rule

  • Related