Disambiguate Class-Member in Multiple Inheritance

Disambiguate class-member in multiple inheritance

Here's a simpler example:

template <typename T>
class Base2 {
public:
void foo(T ) { }
};

struct Derived: public Base2<int>,
public Base2<double>
{};

int main()
{
Derived().foo(0); // error
}

The reason for that comes from the merge rules [class.member.lookup]:

Otherwise (i.e., C does not contain a declaration of f or the resulting declaration set is empty), S(f,C) is
initially empty. If C has base classes, calculate the lookup set for f in each direct base class subobject Bi,
and merge each such lookup set S(f,Bi) in turn into S(f,C).

— [..]

— Otherwise, if the declaration sets of S(f,Bi) and S(f,C) differ, the merge is ambiguous...

Since our initial declaration set is empty (Derived has no methods in it), we have to merge from all of our bases - but our bases have differing sets, so the merge fails. However, that rule explicitly only applies if the declaration set of C (Derived) is empty. So to avoid it, we make it non-empty:

struct Derived: public Base2<int>,
public Base2<double>
{
using Base2<int>::foo;
using Base2<double>::foo;
};

That works because the rule for applying using is

In the declaration set, using-declarations are replaced by the set
of designated members that are not hidden or overridden by members of the derived class (7.3.3),

There's no comment there about whether or not the members differ - we effectively just provide Derived with two overloads on foo, bypassing the member name lookup merge rules.

Now, Derived().foo(0) unambiguously calls Base2<int>::foo(int ).


Alternatively to having a using for each base explicitly, you could write a collector to do them all:

template <typename... Bases>
struct BaseCollector;

template <typename Base>
struct BaseCollector<Base> : Base
{
using Base::foo;
};

template <typename Base, typename... Bases>
struct BaseCollector<Base, Bases...> : Base, BaseCollector<Bases...>
{
using Base::foo;
using BaseCollector<Bases...>::foo;
};

struct Derived : BaseCollector<Base2<int>, Base2<std::string>>
{ };

int main() {
Derived().foo(0); // OK
Derived().foo(std::string("Hello")); // OK
}

In C++17, you can pack expand using declarations also, which means that this can be simplified into:

template <typename... Bases>
struct BaseCollector : Bases...
{
using Bases::foo...;
};

This isn't just shorter to write, it's also more efficient to compile. Win-win.

When using multiple inheritance, why is this qualified name ambiguous?

The problem is that C++ has no way to directly express the concept of "multiple-level" class members, such as "the member x of the Top subobject of Left". What Left::Top::x means is "the member x in the type denoted by Left::Top" - and the type denoted by Left::Top is exactly Top.

This is why you can write odd things like

int Left::* ptr = &Right::Top::x;

because the right hand side of the = is exactly equivalent to &Top::x, and a pointer-to-base-class-member is implicitly convertible to a pointer-to-derived-class-member. (The result of this conversion still refers to the member in the base-class subobject of the derived class.)

To disambiguate, you can either do something along the lines of static_cast<Left &>(b).Top::x or use a pointer-to-member - given int Left::* ptr = &Top::x;, b.*ptr will refer to the x in the Top subobject of the Left subobject of b.

Multiple inheritance from a pack expansion


Does it just inherit every type passed into the varargs?

Yes. It inherits publicly from each of the passed arguments. A simplified version is given below.

From Parameter pack's documentation:

Depending on where the expansion takes place, the resulting comma-separated list is a different kind of list: function parameter list, member initializer list, attribute list, etc. The following is the list of all allowed contexts:

  • Base specifiers and member initializer lists:

    A pack expansion may designate the list of base classes in a class declaration.

Example



struct Person 
{
Person() = default;
Person(const Person&)
{
std::cout<<"copy constrcutor person called"<<std::endl;
}
};
struct Name
{
Name() = default;
Name(const Name&)
{
std::cout<<"copy constructor Name called"<<std::endl;
}
};

template<class... Mixins>
//---------------vvvvvvvvv---------->used as list of base classes from which X inherits publicly
class X : public Mixins...
{
public:
//-------------------------------vvvvvvvvvvvvvvvvv---->used as member initializer list
X(const Mixins&... mixins) : Mixins(mixins)... {}
};
int main()
{
Person p;
Name n;
X<Person, Name> x(p, n); //or even just X x(p, n); works with C++17 due to CTAD
return 0;
}

The output of the above program can be seen here:

copy constrcutor person called
copy constructor Name called
constructor called

Explanation

In the above code, the X class template uses a pack expansion to take each of the supplied mixins and expand it into a public base class. In other words, we get a list of base classes from which X inherits publicly. Moreover, we also have a X constructor that copy-initializes each of the mixins from supplied constructor arguments.

C++ multiple inheritance private member ambigious access

In C++, name-lookup happens before member access checking is performed. Hence, name-lookup (Unqualified in your case) finds two names, and that's ambiguous.

You can use a qualified name to disambiguate:

int main() {
M m;
m.A1::x; //qualifed name-lookup
}

Ambiguous multiple inheritance of template classes

The lookup rules for member names say that your code is ambiguous, because the name is found in two base classes and therefore the lookup set is invalid. You don't need to be familiar with all the details of lookup sets and merging; the important detail is that both base classes are checked and the name add_listener is found in both, which creates an ambiguity.

The easy fix is to bring those base class names into A with using-declarations. This means that both versions of add_listener are looked up in A, rather than in the base classes, so there is no merge ambiguity:

struct A : public Notifier< TimeListener >
, public Notifier< SpaceListener >
{
using Notifier<TimeListener>::add_listener;
using Notifier<SpaceListener>::add_listener;
//plus any more base classes
};

Live Demo

Ambiguous inheritance of function when multiple inheritance of classes that themselves have diamond inheritance in their hierarchy

Because of the virtual inheritance, there is only one vtable for the base class Interface_Partial - once you use virtual inheritance, the "virtualness" infects all derived classes at all levels

The inheritance is ambigous because MyClass has two different version of f() available - one from ClassA and one from ClassB. Because of the virtual inheritance of Interface_Partial, you have two derived-class implementations that are at the same level and are trying to override the same virtual function. Declaring a virtual base class makes all derived classes share the virtual base class, including its vtable. The shared vtable gets updated to contain the pointer of the virtual function which should be called. But since there are two equally "good" ones to choose from, there is no way to pick one over the other.

In the other example you give, Interface_Partial is a non-virtual base class for ClassX and ClassY, so each class is overriding a completely different virtual function. This is unambigous to the compiler, although when you call them, you have to specify which specific f() you want to call.


You can resolve this by providing an implementation of f() in MyClass.



Related Topics



Leave a reply



Submit