Name lookup is a separate phase to overload resolution.
Name lookup occurs first. That is the process of deciding which scope the name applies to. In this case we must decide whether d.foo
means d.D::foo
, or d.B1::foo
, or d.B2::foo
. The name lookup rules do not take into account function parameters or anything; it is purely about names and scopes.
Only once that decision has been made, do we then perform overload resolution on the different overloads of the function in the scope where the name was found.
In your example, calling d.foo()
would find D::foo()
if there were such a function. But there is none. So, working backwards up the scopes, it tries the base classes. Now foo
could equally look up to B1::foo
or B2::foo
so it is ambiguous.
For the same reason, you would get ambiguity calling unqualified foo(5);
inside a D
member function.
The effect of the recommended solution:
struct Derived : public Base1, public Base2{
using Base1::foo;
using Base2::foo;
is that this creates the name D::foo
, and makes it identify two functions. The result is that d.foo
resolves to d.D::foo
, and then overload resolution can happen on these two functions that are identified by D::foo
.
Note: In this example D::foo(int)
and Base1::foo(int)
are two identifiers for the one function; but in general, for the name lookup and overload resolution process, it doesn't make a difference whether they are two separate functions or not.