4

I'm trying to fully understand a C++20 new feature, Implicit creation of objects.

There is this example in the proposal, section "3.3 Type punning":

We do not wish examples such as the following to become valid:

float do_bad_things(int n) {
    alignof(int) alignof(float) char buffer[max(sizeof(int), sizeof(float))];
    *(int*)buffer = n;                      // #1
    new (buffer) std::byte[sizeof(buffer)]; // #X
    return (*float*)buffer;                 // #2
}

The proposed rule would permit an int object to spring into existence to make line #1 valid (in each case), and would permit a float object to likewise spring into existence to make line #2 valid.

Why is the line marked #X (by me) necessary? Does it make a difference? Wouldn't the example be exactly the same, if this line weren't there?

My reasoning is: buffer is a char array, so it implicitly creates objects. So, at line #1, an int is implicitly created. Likewise, at line #2, a float gets implicitly created, even without line #X (because buffer already has the implicitly-creates-objects property). So it seems that line #X doesn't add anything. Am I wrong?

Nicol Bolas
  • 449,505
  • 63
  • 781
  • 982
geza
  • 28,403
  • 6
  • 61
  • 135

1 Answers1

7

Why is the line marked #X (by me) necessary?

Because that invokes implicit object creation.

Implicit object creation (IOC) in C++20 isn't chaos. It isn't "every object exists in every possible memory location at every time." It is instead kind of a quantum state: when IOC rules are invoked over a piece of memory, there is one object that is created. You just don't know what it is. When you actually use the memory for a particular object, it turns out that this is the object (or one compatible with it) that was created at the time IOC was invoked on the memory.

And if you do anything to the storage such that a single object cannot satisfy both, then you get UB.

A piece of memory cannot hold an int and a float within their lifetimes at the same time. IOC doesn't change that.

Line 1 uses IOC to create an int in the storage; at that point, it is functionally no different from int buffer;. Line 2 attempts to access a float in that storage, but no such object exists. If there's an int already there, IOC can't create a float on top of it.

Line X ends the lifetime of all objects in that memory by reusing the storage; there is no int there any longer. And since the object being created is a byte array, this also re-blesses the storage for IOC. This is what makes Line 2 work.

Nicol Bolas
  • 449,505
  • 63
  • 781
  • 982
  • So does this feature work like an oracle? At `char buffer[...]`, we don't know what the buffer will be used for. Still, that line will create an `int` object, because the program will use it as an `int` in the future? I ask this, because you say that Line 1 creates the `int` object, which contradicts my current understanding. – geza Apr 26 '20 at 14:31
  • Where does the create happen? The proposal says "Creation of an array of char, unsigned char, or std::byte implicitly creates objects within that array". So it seems that creation happens at `char buffer[...]`, and not at Line 1. Of course, this difference may not be observable, but could help how I think about this feature. – geza Apr 26 '20 at 14:38
  • @geza: "*So does this feature work like an oracle?*" Basically. I like to think of it as a quantum state. It always had the right object; you just don't know it yet. And yes, the actual creation is stated to happen at the "implicitly creates objects" point, not the place where you use it. That's why I like to think of it as a quantum state. – Nicol Bolas Apr 26 '20 at 14:51