3

If memory is set aside for an object (e.g., through a union) but the constructor has not yet been called, is it legal to call one of the object's non-static methods, assuming the method does not depend on the value of any member variables?

I researched a bit and found some information about "variant members" but I couldn't find info pertaining to this example.

class D {
 public:
  D() { printf("D constructor!\n"); }
  int a = 123;
  void print () const {
    printf("Pointer: %p\n", &a);
  };
};

class C {
 public:
  C() {};
  union {
    D memory;
  };
};

int main() {
  C c;
  c.memory.print();
} 

In this example, I'm calling print() without the constructor ever being called. The intent is to later call the constructor, but even before the constructor is called, we know where variable a will reside. Obviously the value of a is uninitialized at this point, but print() doesn't care about the value.

This seems to work as expected when compiling with gcc and clang for c++11. But I'm wondering if I'm invoking some illegal or undefined behavior here.

1 Answers1

5

I believe this is undefined behavior. Your variant member C::memory has not been initialized because the constructor of C does not provide an initializer [class.base.init]/9.2. Therefore, the lifetime of c.memory has not begun at the point where you call the method print() [basic.life]/1. Based on [basic.life]/7.2:

Similarly, before the lifetime of an object has started but after the storage which the object will occupy has been allocated or, after the lifetime of an object has ended and before the storage which the object occupied is reused or released, any glvalue that refers to the original object may be used but only in limited ways. […] The program has undefined behavior if:

  • […]
  • the glvalue is used to call a non-static member function of the object, or
  • […]

emphasis mine

Note: I am referring to the current C++ standard draft above, however, the relevant wording is basically the same for C++11 except that, in C++11, the fact that D has non-trivial initialization is crucial as what you're doing may otherwise potentially be OK in C++11…

Michael Kenzel
  • 15,508
  • 2
  • 30
  • 39
  • The fact that `D` has non-trivial initialization is also important here. – Ben Voigt Apr 16 '19 at 03:40
  • @BenVoigt Please correct me if I'm wrong, but since `memory` is a variant member, its lifetime should never begin without an explicit intialization being performed, the fact that it has non-trivial initialization doesn't seem to me to have an influence in this particular case!? – Michael Kenzel Apr 16 '19 at 03:52
  • If it has trivial initialization, its lifetime begins the moment (properly sized and aligned, etc) storage is allocated. – Ben Voigt Apr 16 '19 at 03:53
  • 1
    Ah, the wording in your link `[basic.life]/1` is *very* different from C++11. Applicable wording: "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-trivial initialization, its initialization is complete." – Ben Voigt Apr 16 '19 at 03:54
  • 1
    @BenVoigt indeed, that is very interesting. It would seem that the wording has changed to the current form in C++17. I wonder if that won't break some code as the new wording would seem to be a non-conservative change from the pervious one… – Michael Kenzel Apr 16 '19 at 03:57
  • I wonder if the intended interpretation of the new wording is that vacuous initialization is never incomplete, so again objects not scheduled for non-vacuous initialization begin life as soon as storage is obtained. – Ben Voigt Apr 16 '19 at 04:01
  • @BenVoigt so that means that, if `D` had trivial initialization, it would seem to be OK to call a member function on it in C++11 while in C++17 it may not be!? – Michael Kenzel Apr 16 '19 at 04:03
  • I think it would be allowed in both. Naturally any members of `D` would be uninitialized (unless static storage duration in which case zero-initialized) and therefore the static member function would have to be careful not to use the uninitialized values. – Ben Voigt Apr 16 '19 at 04:13
  • @BenVoigt the text you quote is meant to be interpreted like this: "Given that an object is beginning its lifetime, the exact moment it begins is when storage with the proper alignment and size is obtained [etc.]" . It doesn't mean that obtaining storage for an object implies that an object was created. – M.M Apr 16 '19 at 04:27
  • @M.M: So you mean that if we take out the non-trivial constructor, the current code is UB, but this variation is legal? `C c; c.memory.print(); new (&c.memory) D;` ? That's ludicrous. The plain meaning is that an object exists, live, as soon as storage for it is obtained. – Ben Voigt Apr 16 '19 at 04:44
  • @BenVoigt `C c; c.memory.print();` is UB since no object has been created. [See this question](https://stackoverflow.com/questions/37644977/) for more detailed analysis – M.M Apr 16 '19 at 04:50
  • @M.M: The problem is that your interpretation requires time-travel. "Given that an object is beginning its lifetime, the exact moment it begins" – Ben Voigt Apr 16 '19 at 04:54
  • (And yes, I know that time-travel is allowed as a result of UB, but it isn't allowed as a result of valid well-defined code sequences) – Ben Voigt Apr 16 '19 at 04:54
  • @BenVoigt this is already addressed by TC's answer, I've no interest in rehashing those comments again everywhere that the issue comes up. Anyway it is moot for this particular question since the standard explicitly says that a union member's lifetime does not begin in this case. (basic.life/1) – M.M Apr 16 '19 at 05:11