Malvineous
Malvineous

Reputation: 27300

Virtual inheritance, default constructors and extra copying

If you look at the code below, you will see that there are classes A, B and C which each inherit from the last. The inheritance is virtual, meaning that C has to call A's constructor, even though it doesn't inherit directly from A.

This is all good and well, but there are two ways to handle this and they both seem hacky to me, so I'd like to know whether this really is how you are supposed to do it.

The first way (the code below as it is) appears to call A::A(a) twice, once in B's constructor and then again in C's constructor. Of course it doesn't get constructed twice, but a side effect of this is that the parameter a gets copied an extra time, just so it can be passed to B::B(a, b) even though that function doesn't end up using the value. It only needs the value to construct A::A(a), but here that constructor is called from C instead, so the copy of a passed to B::B(a, b) is simply thrown away.

To avoid this wasted copy, the alternative is to create additional constructors in the base classes (uncomment the code below for this option.) This avoids the wasted copy, however the drawback is that you as the programmer are creating an entirely new constructor for your classes, which is never actually called by the code at runtime. You have to declare the function, implement it (even if it's empty) and actually write code that calls it, even though that code will never actually run! This sort of thing normally signals a broken design somewhere, so I am wondering whether there is another way you are supposed to achieve this that is less dodgy.

#include <iostream>

struct Data {
    Data() {
        std::cout << "Creating data\n";
    }
    Data(const Data& d) {
        std::cout << "Copying data\n";
    }
};

struct A {
    A(Data a)
    {
        std::cout << "A(a)\n";
    }
/*
    A()
    {
        std::cout << "A()\n";
    }
*/
};

struct B: virtual public A {
    B(Data a, Data b)
        :   A(a)
    {
        std::cout << "B(a, b)\n";
    }
/*
    B(Data b)
    {
        std::cout << "B(b)\n";
    }
*/
};

struct C: virtual public B {
    C(Data a, Data b, Data c)
        :   A(a),
            B(a, b)
/*
            B(b)
*/
    {
        std::cout << "C(a, b, c)\n";
    }
};

int main(void)
{
    Data a, b, c;
    std::cout << "-- B --\n";
    B bb(a, b);
    std::cout << "-- C --\n";
    C cc(a, b, c);

    return 0;
}

EDIT: Just to clarify, a few people have made the comment that I should just pass by reference. Ignoring the fact that this isn't always what you want (think smart pointers), it doesn't remove the fact that you're passing a value to a function that never gets used, which seems dodgy. So the choices are either pass a value that gets thrown away silently (and hope you never get bitten by that) or implement a function you will never call (and hope that never comes back to bite either.)

So my original question still stands - are these really the only two ways to do this? You have to choose between a superfluous variable or a superfluous function?

Upvotes: 2

Views: 95

Answers (2)

Mateusz Grzejek
Mateusz Grzejek

Reputation: 12058

I am not sure, if that was your intention, but you are doing A LOT of unnecessary copies of Data, because every parameter is passed by value. That means, every time A::A(a) is called, new instance of Data is created, which is copy-constructed from a parameter. In B::B(a,b) two such copies are made and in C::C(a,b,c) - three copies. But note, that B::B(a,b) calls A::A(a) and C::C(a,b,c) calls B::B(a,b), so I guess, that calling C::C(a,b,c) actually creates six copies of Data:

  • three copies of a
  • two copies of b
  • one copy of c

You can avoid this, by passing constructor's arguments by reference - then, no copy is made, unless you, in constructor body or initialization list, will decide to make one.

Try this:

#include <iostream>

struct Data {
    Data() {
        std::cout << "Creating data\n";
    }
    Data(const Data& d) {
        std::cout << "Copying data\n";
    }
};

struct A {
    A(const Data& a)
    {
        std::cout << "A(a)\n";
    }
/*
    A()
    {
        std::cout << "A()\n";
    }
*/
};

struct B: virtual public A {
    B(const Data& a, const Data& b)
        :   A(a)
    {
        std::cout << "B(a, b)\n";
    }
/*
    B(Data b)
    {
        std::cout << "B(b)\n";
    }
*/
};

struct C: virtual public B {
    C(const Data& a, const Data& b, const Data& c)
        :   A(a),
            B(a, b)
/*
            B(b)
*/
    {
        std::cout << "C(a, b, c)\n";
    }
};

int main(void)
{
    Data a, b, c;
    std::cout << "-- B --\n";
    B bb(a, b);
    std::cout << "-- C --\n";
    C cc(a, b, c);

    return 0;
}

Output:

Creating data
Creating data
Creating data
-- B --
A(a)
B(a, b)
-- C --
A(a)
B(a, b)
C(a, b, c)

EDIT:

I see, that I misunderstood your problem. Now, I am not sure if I understand it at all. After compiling your original code:

[Output]

[Cut]
-- C --
Copying data // copy of c in C::C()
Copying data // copy of b in C::C()
Copying data // copy of a in C::C()
Copying data // copy of a passed to A::A()
A(a)
Copying data // copy of b passed to B::B()
Copying data // copy of a passed to B::B()
B(a, b)
C(a, b, c)

Now, which copies you want to get rid of?

You wrote, that A::A() is called twice - in B::B() and C::C(). But you create two objects, one of type B and one of type C. Both these types inherit from A, so A::A() will always be called - either explicitly or not. If you don't call A::A() explicitly, default constructor for A will be called. If it's not defined, compiler error will be generated (try to remove call to B(a, b) from C::C()).

Upvotes: 2

David Haim
David Haim

Reputation: 26476

The way you use Virtual inheritance is wrong, and in this question - there is no relation between the "extra copies" and virtual inheritance

Virtual inheritance is done in order to solve the Diamond problem - if both Horse and Bird inherit from "Animal" , and Pegasus inherit both from Horse and Bird - we don't want every member variable in "Animal" to be contained twice .thus we inherit Horse and Bird virtually in order to have Animal Object only once in Pegasus.

here , this example doesn't present the diamond problem hence doesn't reflect any real use of virtual inheritance (other then it forces A ctor to be called first.)

also , the reason that there are so many copies is because you pass everything by value and not as const reference as you should. passing everything as references will elude most of the copies here.

Upvotes: 1

Related Questions