Reputation: 31
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
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
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
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