I recently received the following question in email from Vijay Visana. I’ve slightly edited it for brevity and/or flow. Vijay writes:
While tinkering with multiple-inheritance in C++ I have come across one peculiarity that baffled me a lot.
A derived (multiple inheritance – no virtual base class) class having all pure abstract base classes can have multiple copies of a distant base class embedded in it and can call that class’s methods without ambiguity.
In Figure 1, C implements all the Pure ABC methods of all the above pure Abstract Base Classes. When I call method of C as following
C* p = new C;
p->Method_of_A(); // though it has two ways to reach A no ambiguity
Let’s pause here for a moment: Do you see why there’s no ambiguity? And did you notice the interesting tidbit of information in the brief description of C’s implementation?
But let’s read on and see the rest of the question:
Now in Figure 2 I twist the pure ABC hierarchy to introduce a closer path. Here C again implements all the Pure ABC methods of all the above pure Abstract Base Classes. When I call method of C as following
B4* p = new C;
p->Method_of_A();At this point of time compiler (vc++) finds it ambiguous. I have seen that an adjustor thunk is being created (when I remove this dubious method call and debug it) for it. Virtual inheritance can solve the problem but I want to know just out of plain curiosity to understand the implementation of MI in C++ (or rather in VC++).
Okay, let’s look into this.
The difference in behavior has nothing to do with the complexity of the inheritance hierarchy or with vtables or thunking. Rather, it has to do with name lookup (in this case, finding "Method_of_A") which in turn has to do whether the static type of the object has the function you want, or whether the compiler has to look further (into base classes) to find the name.
Here’s a quick recap of what happens when we write a function call in C++:
- First comes name lookup: The compiler looks around to find a function having the requested name. It starts in the current scope (in these cases, the scope of the class we’re calling the member function on) and makes a "candidate list" of all functions having that name; if the list is empty, it goes outward to the next enclosing scope (e.g., namespace or base class) and repeats. If it makes it all the way out to the global scope and still finds no candidates, sorry Charlie, you get "name not found."As soon as a scope is encountered that has at least one function with the requested name, the compiler goes to step two.
- Second comes overload resolution: If the candidate list has more than one function in it, the compiler attempts to find a unique best match based on the argument and parameter types. If two or more functions are equally good (or bad) matches, sorry Charlie, you get "ambiguous call."
- Third comes accessibility checking: Finally, the compiler looks to see whether you may actually call the function (e.g., that it’s not private). If you don’t have clearance to call the function, sorry Charlie, you’re not calling from within a derived class, a member function, or friend function and you should have thought of that before trying to access an inaccessible protected or private function. For shame.
I’ve written more about this in my books and articles. Here are a few that are online:
- Namespaces and the Interface Principle (see subhead "Digression: Recap of a Familiar Inheritance Issue"; with notes about name lookup in templates)
- What’s In a Class? – The Interface Principle (search for "name lookup")
- GotW #30: Name Lookup (with notes about argument-dependent lookup, aka ADL, aka Koenig Lookup)
All three steps consider only the static type of the object. Here’s the key interesting information that makes the first example work:
In Figure 1, C implements all the Pure ABC methods of all the above pure Abstract Base Classes.
In other words, there is a function C::Method_of_A. So when the reader did this
C* p = new C;
p->Method_of_A(); // though it has two ways to reach A no ambiguity
the comment is really a red herring because name lookup is not reaching up to A at all. Rather, this code is invoking C::Method_of_A directly.
The second example is needlessly complex, but the key here is that the most-derived class is still the one actually implementing the overrides, but now B4 doesn’t. So when we do this:
B4* p = new C;
p->Method_of_A();
we’re using the object as a B4 and trying to invoke B4::Method_of_A, but since B4 doesn’t provide one itself, name lookup starts looking up through the base classes and finds two equal candidates that it can’t resolve using overload resolution, and so the call is ambiguous.
Name lookup is done based on the static type of the object, or the "type that we’re using it as right now," not on its dynamic type, or the type it really is (the two happen to be the same in Figure 1, and different in Figure 2, which contributed to the confusion). In Figure 1, the static type of the object p points to is C, because p has type pointer to C (as opposed to, say, pointer to some base of C). In Figure 2, the static type is B4, which does not implement Method_of_A and so name lookup goes off into the tree of base classes, finds equal candidates that have identical signatures and so aren’t distinguishable by overload resolution, and the call is ambiguous — irrespective of the fact that the object’s dynamic type happens to be C which uniquely implements Method_of_A. We’re using it as a B4, and so a B4 it shall be… for name lookup purposes, at any rate.
Hi Herb,
My concern was from polymorphism point of view. Isn’t it defying concept of polymorphism?
Regards
Vijay Visana
Herb,
Interestingnthoughts on the effect of static types of objects on the function namenlookup. I had the chance to follow-up on your recommended readings andnespecially the article about the "interface principle". I am not quitensure if I agree if a free function foo(const X&) should benconsidered a part of a class X since it is a part of theninterface_of_X (given that its the same header file as X). For me, thenissue of whether the class is in the same header file as the freenfunction seems irrelevant in deciding if something is part of a classnor not since a "class" is a generic OO concept and other languages suchnas Java or C# do not employ header files (or free functions for thatnmatter). Therefore any discussion on what constitutes a class should benindependent of compiler and language. To me a class acts as a templatento create instances of its type and expose a related real-worldnconcept. It contains data and exposes methods to its "client" objects.nThe key to me is that the class definition itself decides what methodsnare exposed as public vs. those that are merely protected or private.nIt exercises no such control on a free function. Further foo(..) cannnot access private members of the class X as its member functions can.nTo count a free function as a part of X atleast in theory, is to breaknthe "unit" nature of a class and in my mind creates a set ofnsecond-class citizens on the class’s interface.
Sorry for the double post (and this post apologizing for the double post:)