Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Why does it work when it breaks the rule of order of initialization list

Tags:

c++

Why does this code work? I expected this to fail because of breaking of one of the basic C++ rules:

#include <iostream>
using namespace std;

struct A {
    A() { cout << "ctor A" << endl; }
    void doSth() { cout << "a doing sth" << endl; }
};

struct B {
    B(A& a) : a(a) { cout << "ctor B" << endl; }

    void doSth() { a.doSth(); }

    A& a;
};

struct C {
    C() : b(a) { cout << "ctor C" << endl; }

    void doSth() { b.doSth(); }

    B b;
    A a;
};

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

https://wandbox.org/permlink/aoJsYkbhDO6pNrg0

I expected this to fail since in C's constructor, B is given a reference to object of A when this A object has not yet been created.

Am I missing something? Does the rule of order of initialization being the same as the order of fields not apply for references?

EDIT: What surprises me even more is that I can add a call to "a.doSth();" inside B constructor and this will also work. Why? At this moment the A object should not exist!

like image 815
YotKay Avatar asked Jan 25 '18 11:01

YotKay


4 Answers

Your code is fine so long as the constructor of B doesn't use that reference it gets for anything other than binding its member. The storage for a has already been allocated when the c'tor of C starts, and like Sneftel says, it's in scope. As such, you may take its reference, as [basic.life]/7 explicitly allows:

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. For an object under construction or destruction, see [class.cdtor]. Otherwise, such a glvalue refers to allocated storage ([basic.stc.dynamic.deallocation]), and using the properties of the glvalue that do not depend on its value is well-defined. The program has undefined behavior if:

  • the glvalue is used to access the object, or
  • the glvalue is used to call a non-static member function of the object, or
  • the glvalue is bound to a reference to a virtual base class ([dcl.init.ref]), or
  • the glvalue is used as the operand of a dynamic_­cast or as the operand of typeid.

Regarding your edit:

What surprises me even more is that I can add a call to "a.doSth();" inside B constructor and this will also work. Why? At this moment the A object should not exist!

Undefined behavior is undefined. The second bullet in the paragraph I linked to pretty much says it. A compiler may be clever enough to catch it, but it doesn't have to be.

like image 60
StoryTeller - Unslander Monica Avatar answered Nov 16 '22 19:11

StoryTeller - Unslander Monica


In your code snippet, when C is being constructed, a has not been initialized but it is already in scope, so the compiler is not required to issue a diagnostic. Its value is undefined.

The code is fine in the sense that B::a is properly an alias of C::a. The lifetime of the storage backing C::a has already begun by the time B::B() runs.

With respect to your edit: Although C::a's storage duration has already begun, a.doSth() from B::B() would absolutely result in undefined behavior (google to see why something can be UB and still "work").

like image 23
Sneftel Avatar answered Nov 16 '22 20:11

Sneftel


This works because you are not accessing uninitialized field C::a during C::binitialization. By calling C() : b(a) you are binding a reference to a to be supplied for B(A& a) constructor. If you change your code to actually use uninitialized value somehow then it will be an undefined behavior:

struct B {
   B(A& a)
   : m_a(a) // now this calls copy constructor attempting to access uninitialized value of `a`
   { cout << "ctor B" << endl; }

  void doSth() { a.doSth(); }

   A m_a;
};
like image 1
user7860670 Avatar answered Nov 16 '22 20:11

user7860670


Undefined behavior means anything is possible, including appearing to work fine. Doesn't mean it will work fine next week or even the next time you run it - you might get demons flying from your nose.

What's probably going on when you call a.doSth() is that the compiler converts the call to a static a::doSth(); since it's not a virtual function, it doesn't need to access the object to make the call. The function itself doesn't use any member variables or functions so no invalid accesses are generated. It works even though it's not guaranteed to work.

like image 1
Mark Ransom Avatar answered Nov 16 '22 18:11

Mark Ransom