21

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!

YotKay
  • 1,127
  • 1
  • 9
  • 25
  • Yes, but after B' constructor is called. – YotKay Jan 25 '18 at 11:06
  • A reference is just a pointer. You could pass a pointer to it no problems, same for a reference. Try to **use** the object and you'll have problems though. – Jonathan Potter Jan 25 '18 at 11:09
  • 2
    @JonathanPotter a reference is not a pointer. Maybe it is implemented as pointer, but even if this is always the case, a reference is still not a pointer – 463035818_is_not_an_ai Jan 25 '18 at 11:10
  • 1
    You should change field names so they won't clash with constructor arguments. – user7860670 Jan 25 '18 at 11:11
  • A::doSth doesn't need a fully constructed object to work, it could actually be static. Try to print out a class variable in dosth and you will start seeing the effects of the uninitialized class. – Paolo Brandoli Jan 25 '18 at 11:11
  • @VTT yes, but this is not the subject of this question. – YotKay Jan 25 '18 at 11:12
  • @VTT field names dont clash with constructor arguments. Actually I always use `foo::foo(int bar) : bar(bar) {}` when there is no need to introduce just another name – 463035818_is_not_an_ai Jan 25 '18 at 11:12
  • 5
    @VTT There's no reason to do that. – Bartek Banachewicz Jan 25 '18 at 11:12
  • @BartekBanachewicz There are multiple reason to do that. – user7860670 Jan 25 '18 at 11:14
  • 3
    @VTT I'm sorry, I meant to say that there are no *valid* reasons to do that. And [have a read](https://stackoverflow.com/questions/6185020/initializing-member-variables-using-the-same-name-for-constructor-arguments-as-f) – Bartek Banachewicz Jan 25 '18 at 11:15
  • @BartekBanachewicz There are **multiple valid reasons** to do that. – user7860670 Jan 25 '18 at 11:15
  • What do you mean by "works"? Doesn't crash? Outputs the same thing some valid code would output? Hangs in the office 9am to 5pm? All of the above are allowed manifestations of undefined behaviour. – n. m. could be an AI Jan 25 '18 at 11:17
  • @n.m. anything that might cause confusion tends to be bad. Why not make it `a_` and then `a(a_)` so that the reader; if not fully versed with this can understand. Sure, *the ideal is to fix the knowledge gap*; but in the real world environment your code has to be readable by all employees; and the fastest route to that is what the company will be interested in. Sure, it's not a coding reason, but a business reason - but I think it's equally valid. – UKMonkey Jan 25 '18 at 11:35
  • 3
    @UKMonkey `a(a)` is idiomatic C++. It *might* cause confusion at some point for some people, but `a(a_)` *does* add clutter now for everyone. – n. m. could be an AI Jan 25 '18 at 11:52
  • @n.m. I won't believe that you've collected 2k+ c++ reputation without encountering drastic consequences of name collisions multiple times. – user7860670 Jan 25 '18 at 11:52
  • 2
    @VTT there's **no** collision here. – n. m. could be an AI Jan 25 '18 at 11:56
  • @n.m. you're saying that adding a single character to a variable name, likely used just in the initialization list, is clutter? You're saying that not adding it is worth potentially hours of peoples time checking that there's no collision, to the company? – UKMonkey Jan 25 '18 at 11:58
  • 1
    @UKMonkey yes I'm definitely saying this is clutter. I have not seen people wasting hours over this. A couple of minutes maybe (in total). If you think there's a danger of wasted hours, by all means factor it into your coding standards. Anyway this discussion is hardly relevant to the question. – n. m. could be an AI Jan 25 '18 at 12:25
  • Related, possibly dup.: https://stackoverflow.com/questions/37724668/is-taking-uninitialized-references-of-class-members-during-construction-legal – Holt Jan 25 '18 at 12:30

6 Answers6

24

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.

StoryTeller - Unslander Monica
  • 165,132
  • 21
  • 377
  • 458
11

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").

Sneftel
  • 40,271
  • 12
  • 71
  • 104
1

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;
};
user7860670
  • 35,849
  • 4
  • 58
  • 84
1

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.

Mark Ransom
  • 299,747
  • 42
  • 398
  • 622
0

It doesn't "work" in the sense that the a object used for initialization hasn't had its constructor called yet (which your logs reveal) - this means that the init of b might or might not fail depending on what a is doing.

The compiler doesn't prevent that, but I guess it should. Anyway, I don't think this is UB unless you actually try to use the unitialized object; just storing the reference should be fine.

Bartek Banachewicz
  • 38,596
  • 7
  • 91
  • 135
0

It works because B is initialized with a reference, and that reference already exists so it can be used to initialize something with it.

If you try with a being passed by value in ctor of B then the compiler would complain:

warning: field 'a' is uninitialized when used here [-Wuninitialized]

Jean-Baptiste Yunès
  • 34,548
  • 4
  • 48
  • 69