2

Consider:

struct A { void do_something() {} };

struct B {
  A& a;
  B(A& a) : a{a} { a.do_something(); }
};

struct C {
  A a; B b;
  C(A& a) : b{a} {}
};

int main() {
  C c{ c.a };
}

It seems possible that this could be made to work, because:

  • even before c is initialized, we know its memory layout and the address of c.a
  • we don't actually use c.a until it is initialized.

Additionally, I didn't get a warning under a few different compilers.

However, I experienced some extremely odd behaviour (a bit later on) that could only be down to doing something undefined, and which only went away when I reorganised my program to avoid this pattern.

Thanks.

STU
  • 121
  • 5
  • 1
    Accessing `c.a` *outside `C`'s constructor* and before `c`'s lifetime has begun is a problem. It's a bit like `int x = x;`. – Kerrek SB May 24 '16 at 09:34
  • But my program works perfectly! – STU May 24 '16 at 10:55
  • @STU, undefined behavior doesn't mean that the code wouldn't work, (and your current snippet works well, and should work well for every sane compiler).. But UB also means the compiler is permitted to generate code that will reformat your PC; ;-) – WhiZTiM May 24 '16 at 11:00
  • 3
    [CWG1530](http://wg21.link/cwg1530) is relevant in this case, I think. – bogdan May 24 '16 at 11:44
  • @bogdan thanks, this is the answer as far as I'm concerned. – STU Jun 03 '16 at 17:44
  • @STU: I find that assertion highly improbable! – Kerrek SB Jun 03 '16 at 19:12

3 Answers3

2

Your program has undefined behavior, because you are accessing an object's member outside the object and before the lifetime of the object commences.

$12.7: 1: 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...

As to why it compiles fine: A name can be used after its point of declaration ... And Quoting the C++ Standard draft... (emphasis mine):

$3.3.2 : 1: The point of declaration for a name is immediately after its complete declarator (Clause [dcl.decl]) and before its initializer (if any)...

And initalizer is defined to have the syntax (reproduced here partially):

initializer...

initializer:
    brace-or-equal-initializer
    ( expression-list )

brace-or-equal-initializer:
    = initializer-clause
    braced-init-list

initializer-clause:
    assignment-expression
    braced-init-list
. . .

The above is the same reason why this will compile:

int k(k);
int m{m};
int b = b;
WhiZTiM
  • 21,207
  • 4
  • 43
  • 68
  • I'm not sure if the grammar (which just describes how C++ source is parsed) is at all relevant to the runtime. This section just tells us at which point in the source text a particular token starts having a specific meaning, but that is a point in text not a point in time. – MSalters May 24 '16 at 12:21
2

In addition to my previous answer,


Your code is a crafty one... Because, despite the supposed UB with using an object before its initialized, the behaviour of your code is apparently well defined..

How? In the construction of c, the following sequence of events will happen:

  1. You call C's constructor, C(A& a) : b{a} {} which takes a reference to an object of type A. (A reference is just like an address, and as you rightly mentioned, the address of c.a is known at compile time). Your call is: C c{ c.a }; and the compiler is fine with that since c.a is an accessible name

  2. Due to the order of declaration of C's members...

    struct C {
      A a; B b;
      C(A& a) : b{a} {}
    };
    

    the object a is initialized before b.

  3. Thus, a becomes alive before its use in the member initializer... b{a}


But again, you can be smoked by optimizers...

Community
  • 1
  • 1
WhiZTiM
  • 21,207
  • 4
  • 43
  • 68
  • This was my reasoning, but like I said it ended up giving me weird bugs 20 lines later until I fixed it. – STU Jun 03 '16 at 17:42
0

C++ has a pretty well-defined order of construction. First (non-virtual) base classes are initialized. Members are initialized next, in order of their declaration in the class (and ignoring the order of the initializer list), and only after the last member has been initialized will the body of the constructor be entered.

In this case, it means that the ctors are called in the order A::A, B::B, C::C.

You would have an issue if C was declared as

struct C {
  B b; A a; 
  C(A& a) : b{a} {}
};
MSalters
  • 173,980
  • 10
  • 155
  • 350
  • Well, there's still the issue of [12.7p1], which says *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. [...]*. However, this may be adjusted in the future (see my comment on the question). – bogdan May 24 '16 at 12:33
  • @bogdan: I noticed it, read the CWG issue, upvoted you and then wrote the answer. As you can infer from the CWG issue, all "reasonable interpretations" allow the formation of a reference. – MSalters May 24 '16 at 12:38
  • The "reasonable interpretation" in the CWG issue refers to the interpretation of the word *access* in [3.8], and that was clarified in the meantime in the Standard wording, so we agree on that part. I commented because I read your answer to mean "You're fine", and I don't think that's a safe assumption to make as long as [12.7p1] unambiguously makes the code result in UB (in `C c{c.a};`, the expression `c.a` is evaluated before `c`'s constructor starts executing). – bogdan May 24 '16 at 13:50