Search code examples
c++referencedangling-pointer

Why does the initialization of a reference to the base class object with the child class object lead to a dangling reference?


In the code below, I defined 4 classes:

  • class A.
  • base class B_parent.
  • derived from B_parent class B_child which contains a reference to an object of class A, the reference is initialized by the constructor.
  • class C, with two references: object of class A and object of class B_parent, the references can be initialized by two constructors.

The first constructor in class C works perfectly fine (although, in the main(), you can see I initialize it with the derived class object).

The second constructor in class C leads to a dangling reference, and the code crashes with Segmentation fault.

Why is the reference broken in the second case and how can I fix this error?

My goal is to provide a default value for the reference _b in class C, if B_parent object is not provided when I construct object of class C.

The code:

#include <iostream>

using namespace std;

class A
{
public:
    void foo() const
    {
        cout << "foo() from class A\n";
    }
};

class B_parent
{
public:
    virtual void foo() const
    {
        cout << "foo() from class B_parent\n";
    }
};

class B_child : public B_parent
{
    const A &_a;

public:
    B_child(const A &a) : B_parent(), _a(a) {}

    void foo() const
    {
        cout << "foo() from class B_child\n";
        _a.foo();
    }
};

class C
{
    const A &_a;
    const B_parent &_b;

public:
    C(const A &a, const B_parent &b) : _a(a), _b(b) {}
    C(const A &a) : _a(a), _b(B_child(a)) {}

    void foo() const
    {
        cout << "foo() from class C\n";
        _a.foo();
        _b.foo();
    }
};

int main()
{
    A a;
    B_child b(a);

    cout << "Class C (variant 1):\n";
    C c1(a, b); // Calling ctor with A and B_child objects -> OK
    c1.foo();

    cout << "\nClass C (variant 2):\n";
    C c2(a); // Calling ctor with A and B_child(a) from class' init list -> Segmentation fault
    c2.foo();

    return 0;
}

I tried to reorder the variables in the init list, tried to replace the second constructor in class C:

C(const A &a) : _a(a), _b(B_child(a)) {}

with

C(const A &a) : _a(a), _b(B_child(_a)) {}

Still get Segmentation fault.

EDIT:

I managed to solve this using a pointer. Class C can be rewritten as:

class C
{
    const A &_a;
    const B_parent *_b;

public:
    C(const A &a, const B_parent &b) : _a(a), _b(&b) {}
    C(const A &a) : _a(a), _b(new B_child(a)) {}

    void foo() const
    {
        cout << "foo() from class C\n";
        _a.foo();
        _b->foo();
    }
};

However, it doesn't look like a good practice since I create a default object with new B_child(a) but never destroy it. Should I just delete this pointer in the destructor or are there better ways of providing a default object in class C?


Solution

  • First I'll explain where your original code goes wrong.

    C(const A &a) : _a(a), _b(B_child(a)) {}
    

    This creates a temporary object of type B_child, takes a reference to this object and stores it in _b. Then B_child is deleted, because it was a temporary and the reference in _b is left dangling.

    The other constructor, C(const A &a, const B_parent &b), works fine because it takes a reference to an object that lives outside of the constructor call.

    The basic consideration here is: where do your objects live? For the working constructor this is managed by the caller. There can be dangling references, but only if the caller screws up. For the broken constructor, the B object has no place to live, thus the reference is always dangling.

    Now for how to solve this. It very much depends on what you're trying to do.

    If you need the ability to store a base class with virtual methods, then you should use a smart pointer (unique_ptr or shared_ptr). This will take care of the reference counting and deletion for you.

    If you need to be able to either reference an existing B_parent or create a new one, depending on the constructor called, you should also use a smart pointer. In this case, shared_ptr is your only option, because unique_ptr can't reference something that is also held elsewhere.

    Otherwise, if you need neither of those features, _b should just be a value instead of a reference/pointer.