Multiple inheritence leads to spurious ambiguous virtual function overload

You can do this:

struct Baz : public Foo, public Bar
{
    using Bar::do_stuff;
    using Foo::do_stuff;
    //...
}

Tested with wandbox gcc latest and it compiles fine. I think it's the same case with function overloads, once you overload one you can't use base class implementations without using.

In fact this has nothing to do with virtual functions. The following example has the same error GCC 9.2.0 error: reference to 'do_stuff' is ambiguous:

struct Foo
{
    void do_stuff (int, int){}
};

struct Bar
{
    void do_stuff (float) {}
};

struct Baz : public Foo, public Bar
{
    void func ()
    {
        do_stuff (1.1f); // ERROR HERE
    }
};

Possible related question


Name lookup and overload resolution are different. The name must be found in a scope first, i.e. we must find an X so that the name do_stuff is resolved to X::do_stuff -- independently of the usage of the name -- and then overload resolution selects between the different declarations of X::do_stuff.

The process is NOT to identify all such cases A::do_stuff, B::do_stuff, etc. that are visible, and then perform overload resolution amongst the union of that. Instead, a single scope must be identified for the name.

In this code:

struct Baz : public Foo, public Bar
{
    void func ()
    {
        do_stuff (1.1f); // ERROR HERE
    }
};

Baz does not contain the name do_stuff, so base classes can be looked up . But the name occurs in two different bases, so name lookup fails to identify a scope. We never get so far as overload resolution.

The suggested fix in the other answer works because it introduces the name do_stuff to the scope of Baz, and also introduces 2 overloads for the name. So name lookup determines that do_stuff means Baz::do_stuff and then overload resolution selects from the two functions that are known as Baz::do_stuff.


As an aside, shadowing is another consequence of name lookup (not a rule in itself). Name lookup selects the inner scope, and so anything in the outer scope is not a match.

A further complicating factor occurs when argument-dependent lookup is in play. To summarize very briefly, name lookup is done multiple times for a function call with arguments of class type -- the basic version as described in my answer, and then again for each argument's type. Then the union of the scopes found goes into the overload set. But that does not apply to your example since your function only has parameters of built-in type.