Asger Gitz
Asger Gitz

Reputation: 31

Multiple inheritance and accessing ambiguous elements

I'm trying to do some runtime type information tricks through multiple inheritance and templating, but I hit an issue with ambiguity.

First, a very basic example of what I'm talking about:

#include <memory>

struct A { };
struct B : public A { };
struct C : public B, public A { };

int main() {
    std::shared_ptr<A> p0 = std::make_shared<B>(); // possible
    std::shared_ptr<A> p1 = std::make_shared<C>(); // impossible

    // Here the compiler obviously whines "‘A’ is an ambiguous base of ‘C’"
    // but if we're especially naughty, we can circumvent it with a void* cast
    // we still can't really use this pointer to anything though.
    A* p2 = static_cast<A*>((void*)new C()); 
}

As the comments suggest, I can be especially naughty and completely ignore the type. This mischievousness will get me straight to developer hell and won't let me use the pointer as I wish anyway, so I'm actually fine with just having a base of std::shared_ptr<B>. But the second (and slightly closer to reality) example shows that this compromise is not enough:

#include <iostream>
#include <memory>

int current_val = 0;

struct A {
    int runtime_val;
    A(int v) : runtime_val(v) 
    { 
        std::cout << "ACtor with arg: " << v << std::endl; 
    }
};

template<typename T>
struct B : public A {
    static const int global_val;
    B() : A(global_val) {}
};

template<typename T>
const int B<T>::global_val{current_val++};

struct X : public B<X> {};           // Base 
struct C : public X, public B<C> {}; // Leaf
struct D : public X, public B<D> {}; // Leaf

int main() {
    std::cout << "Xval: " << B<X>::global_val << std::endl; // 0
    std::cout << "Dval: " << B<D>::global_val << std::endl; // 1
    std::cout << "Cval: " << B<C>::global_val << std::endl; // 2

    // std::shared_ptr<A> px = std::make_shared<X>(); // Impossible because of ambiguity
    std::shared_ptr<X> p0 = std::make_shared<X>(); // I'm fine with this, really
    std::shared_ptr<X> p1 = std::make_shared<D>();
    std::shared_ptr<X> p2 = std::make_shared<C>();

    std::cout << "p0 val: " << p0->runtime_val << " (X)" << std::endl; // 0     :)
    std::cout << "p1 val: " << p0->runtime_val << " (D)" << std::endl; // 0     :(
    std::cout << "p2 val: " << p0->runtime_val << " (C)" << std::endl; // 0    >:(
}

In this example, I use B<T>::global_val as a sort of runtime type information on a given type <T>. When the program is run I get the following output (I added some extra comments, for clarity):

Xval: 0
Dval: 1
Cval: 2
ACtor with arg: 0  -- (p0 initialization)
ACtor with arg: 0  -- (p1 initialization (X base))
ACtor with arg: 1  -- (p1 initialization (D base))
ACtor with arg: 0  -- (p2 initialization (X base))
ACtor with arg: 2  -- (p2 initialization (C base))
p0 val: 0 (X)
p1 val: 0 (D)
p2 val: 0 (C)

It seems that the v-table only wants to point to the X-base of my D and C classes. How can I ensure that the D and C instances' runtime_val will point to the leaves of the inheritance tree, and not the base?

P.S. I have tried making the X base class purely virtual, but no luck there.

Upvotes: 0

Views: 128

Answers (3)

Asger Gitz
Asger Gitz

Reputation: 31

In the comments, @formerlyknownas-463035818 provided the answer: This is essentially "The Diamond Problem", solvable through Virtual Inheritance:

/* ... */
template<typename T>
struct B : public virtual A { // <---- Virtual inheritance
    static const int global_val;
    B() : A(global_val) {}
};
/* ... */

struct X : public B<X> { 
    X() : A(B<X>::global_val) {} // Implementation classes should then initialize their A's individually
};
struct C : public X, public B<C> {
    C() : A(B<C>::global_val) {} // Implementation classes should then initialize their A's individually
};
struct D : public X, public B<D> { 
    D() : A(B<D>::global_val) {} // Implementation classes should then initialize their A's individually
};

Upvotes: 0

eerorika
eerorika

Reputation: 238331

std::shared_ptr<A> p1 = std::make_shared<C>(); // impossible

To clarify why this is impossible, it is not possible for the compiler to know which one of the A bases you inted to point to.

If you inted to point to the A base of the B base, that is possible with an explicit conversion:

std::shared_ptr<A> p1 = std::static_pointer_cast<B>(std::make_shared<C>());

But there is indeed no way to refer to the direct A base, because of the ambiguity. You could introduce additional wrapper to the other base:

struct A          {};
struct B  : A     {};
struct B2 : A     {};
struct C  : B, B2 {};

std::shared_ptr<A> p1 = std::static_pointer_cast<B >(std::make_shared<C>());
std::shared_ptr<A> p2 = std::static_pointer_cast<B2>(std::make_shared<C>());

Upvotes: 2

Jeffrey
Jeffrey

Reputation: 11400

This looks wrong:

struct B : public A { };
struct C : public B, public A { };

C is a B, which in turn is an A. So asking C to also be an A feels wrong.

My compiler warns with:

1>D:\projects\scratch\scratch.cpp(6,19): warning C4584: 'C': base-class 'A' is already a base-class of 'B' 1>D:\projects\scratch\scratch.cpp(4): message : see declaration of 'A' 1>D:\projects\scratch\scratch.cpp(5): message : see declaration of 'B'

And removing the duplicate inheritance fixes the first snippet:

struct B : public A { };
struct C : public B/*, public A*/ { };

From here, the real question is: Do you need multiple A instances as base ?

Upvotes: 0

Related Questions