20

Let's assume we have a class B that has a member which is default initialized to 42. This class knows how to print the value of its member (it does so in the constructor):

struct B
{
  B() : member(42) { printMember(); }

  void printMember() const { std::cout << "value: " << member << std::endl; }

  int member;
};

Then we add a class A which receives a const reference to a B and asks B to print its value:

struct A
{
  A(const B& b) { b.printMember(); }
};

Finally we add another class Aggregate that aggregates an A and a B. The tricky part is that object a of type A is declared before object b type B, but then a is initialized using a (not yet valid?) reference to b:

struct Aggregate
{
  A a;
  B b;

  Aggregate() : a(b) { }
};

Consider the output of creating an Aggregate (I have added some logging to both the constructor and destructor of A and B) (Try it online!):

a c'tor
value: 0
b c'tor
value: 42
b d'tor
a d'tor

Am I right to assume that it is invalid to initialize a with a reference to a (not yet valid) instance of b and that this is therefore undefined behavior?


I am aware of the initialization order. This is what makes me struggle. I know that b is not yet constructed, but I also think to know that b's future address can be determined even before b is constructed. Therefore I assumed there could be some rule that I am not aware of that allows the compiler to default initialize bs members prior to b's construction or something like that. (It would have been more obvious if the first printed out value would have been something that looks random rather than 0 (the default value of int)).


This answer helped me understand that I need to distinguish between

  • binding a reference to an uninitialized object (which is valid) and
  • accessing by reference an uninitialized object (which is undefined)
Peter Mortensen
  • 30,738
  • 21
  • 105
  • 131
simon
  • 273
  • 1
  • 7
  • 2
    If you switch their order in the class definition, it will have better defined behavior. Alternatively, don't use the reference to access the object (see here for a related question https://stackoverflow.com/questions/50020255/in-the-member-initializer-list-can-i-create-a-reference-to-a-member-variable-no). – StoryTeller - Unslander Monica Aug 27 '18 at 08:28
  • 1
    I know how to "fix" it. What I am not sure about is, whether it *should* be fixed (as it's ugly) or *must* be fixed (because it's undefined behavior and therefore simply broken) ;) – simon Aug 27 '18 at 08:35
  • 2
    What you display *is broken*. The example in the other question is not. It's a mine-field of standardese – StoryTeller - Unslander Monica Aug 27 '18 at 08:37
  • 4
    You are using object before it has been initialized. Why do you think you shouldn't fix it? – Daniel Langr Aug 27 '18 at 08:40
  • Possible duplicate of [C++: Initialization Order of Class Data Members](https://stackoverflow.com/questions/2669888/c-initialization-order-of-class-data-members) – Daniel Langr Aug 27 '18 at 08:41
  • 3
    `b` is not valid. The *reference to `b`* is valid, but it is a reference to an object that was not constructed yet; it can be used in certain limited ways, but not in the way you have used it. – n. m. could be an AI Aug 27 '18 at 09:29
  • @n.m. I very much like this comment. It certainly helps me understand my actual problem. – simon Aug 27 '18 at 09:58
  • The relevant section of the draft standard is \[basic.life], paragraph 7 ("Similarly, before the lifetime of an object has started...") – n. m. could be an AI Aug 27 '18 at 10:20
  • Related to [Is passing a C++ object into its own constructor legal?](https://stackoverflow.com/q/32608458/1708801) ... the answer is here but the question is not exactly the same. – Shafik Yaghmour Aug 27 '18 at 13:07

2 Answers2

23

Yes, you are right that it is UB, but for different reasons than just storing a reference to an object that hasn't been constructed.

Construction of class members happens in order of their appearance in the class. Although the address of B is not going to change and technically you can store a reference to it, as @StoryTeller pointed out, calling b.printMember() in the constructor with b that hasn't been constructed yet is definitely UB.

Peter Mortensen
  • 30,738
  • 21
  • 105
  • 131
Dev Null
  • 4,731
  • 1
  • 30
  • 46
1

The order of initialization of class members is as below.

From CPP standard (N4713), the relevant portion is highlighted:

15.6.2 Initializing bases and members [class.base.init] ...
13 In a non-delegating constructor, initialization proceeds in the following order:
(13.1) — First, and only for the constructor of the most derived class (6.6.2), virtual base classes are initialized in the order they appear on a depth-first left-to-right traversal of the directed acyclic graph of base classes, where “left-to-right” is the order of appearance of the base classes in the derived class base-specifier-list.
(13.2) — Then, direct base classes are initialized in declaration order as they appear in the base-specifier-list (regardless of the order of the mem-initializers).
(13.3) — Then, non-static data members are initialized in the order they were declared in the class definition (again regardless of the order of the mem-initializers).
(13.4) — Finally, the compound-statement of the constructor body is executed.
[ Note: The declaration order is mandated to ensure that base and member subobjects are destroyed in the reverse order of initialization. —end note ]

P.W
  • 26,289
  • 6
  • 39
  • 76
  • `By the time, the constructor of Aggregate is called, the constructors of a (first) and b (next) are already called` — you seem to be confusing `Aggregate` and `A`, or something else. The statement doesn't appear to make sense. – Ruslan Aug 27 '18 at 09:55
  • @Ruslan: Edited, see if it appears proper now. – P.W Aug 27 '18 at 10:11
  • No, I still don't get what you mean by that sentence. What exactly is not undefined behavior? The UB has already happened by the time that initialization is done — at the stage of calling the constructor of `a`. – Ruslan Aug 27 '18 at 10:17
  • Yes, UB has happened at the construction of a. But after its construction, `member` has a certain value, whatever that value is. So using that value in construction of Aggregate is not UB. But the overall behaviour is UB. – P.W Aug 27 '18 at 10:23
  • 1
    No it hasn't. After UB you can't tell the state of the program — the program may no longer exist, or appear to be in multiple states at the same time or whatever... – Ruslan Aug 27 '18 at 10:25
  • And since UB allows time travel (or at least overwriting past results wherever they might be stored), you can't even reason backwards _before_ UB. – MSalters Aug 27 '18 at 13:59