I would like to implement interactions between two objects whose types are derived from a common base class. There is a default interaction and specific things may happen once objects of the same type interact.
This is implemented using the following double dispatch scheme:
#include <iostream>
class A
{
public:
virtual void PostCompose(A* other)
{
other->PreCompose(this);
}
virtual void PreCompose(A* other)
{
std::cout << "Precomposing with an A object" << std::endl;
}
};
class B : public A
{
public:
virtual void PostCompose(A* other) // This one needs to be present to prevent a warning
{
other->PreCompose(this);
}
virtual void PreCompose(A* other) // This one needs to be present to prevent an error
{
std::cout << "Precomposing with an A object" << std::endl;
}
virtual void PostCompose(B* other)
{
other->PreCompose(this);
}
virtual void PreCompose(B* other)
{
std::cout << "Precomposing with a B object" << std::endl;
}
};
int main()
{
A a;
B b;
a.PostCompose(&a); // -> "Precomposing with an A object"
a.PostCompose(&b); // -> "Precomposing with an A object"
b.PostCompose(&a); // -> "Precomposing with an A object"
b.PostCompose(&b); // -> "Precomposing with a B object"
}
I have two, unfortunately quite different questions regarding this code:
- Do you think this is a reasonable approach? Would you suggest something different?
- If I omit the first two
B
methods, I get compiler warnings and errors that the last two B
methods hide the A
methods. Why is that? An A*
pointer should not be cast to a B*
pointer, or should it?
Update: I just found out that adding
using A::PreCompose;
using A::PostCompose;
makes the errors and warnings vanish, but why is this necessary?
Update 2: This is neatly explained here: http://www.parashift.com/c++-faq-lite/strange-inheritance.html#faq-23.9, thank you. What about my first question? Any comments on this approach?
Double dispatch is usually implemented differently in C++, with the base class having all different versions (which makes it a maintenance nightmare, but that is how the language is). The problem with your attempt to double dispatch is that dynamic dispatch will find the most derived type B
of the object on which you are calling the method, but then the argument has static type A*
. Since A
does not have an overload that takes B*
as argument, then the call other->PreCompose(this)
will implicitly upcast this
to A*
and you are left with single dispatch on the second argument.
As of the actual question: why is the compiler producing the warnings? why do I need to add the using A::Precompose
directives?
The reason for that are the lookup rules in C++. Then the compiler encounters a call to obj.member()
, it has to lookup the identifier member
, and it will do so starting from the static type of obj
, if it fails to locate member
in that context it will move up in the hierarchy and lookup in the bases of the static type of obj
.
Once the first identifier is found, lookup will stop and try to match the function call with the available overloads, and if the call cannot be matched it will trigger an error. The important bit here is that lookup will not look further up in the hierarchy if the function call cannot be matched. By adding the using base::member
declaration, you are bringing the identifier member
from the base class into the current scope.
Example:
struct base {
void foo( const char * ) {}
void foo( int ) {}
};
struct derived : base {
void foo( std::string const & ) {};
};
int main() {
derived d;
d.foo( "Hi" );
d.foo( 5 );
base &b = d;
b.foo( "you" );
b.foo( 5 );
d.base::foo( "there" );
}
When the compiler encounters the expression d.foo( "Hi" );
the static type of the object is derived
, and lookup will check all member functions in derived
, the identifier foo
is located there, and lookup does not proceed upwards. The argument to the only available overload is std::string const&
, and the compiler will add an implicit conversion, so even if there could be a best potential match (base::foo(const char*)
is a better match than derived::foo(std::string const&)
for that call) it will effectively call:
d.derived::foo( std::string("Hi") );
The next expression d.foo( 5 );
is processed similarly, lookup starts in derived
and it finds that there is a member function there. But the argument 5
cannot be converted to std::string const &
implicitly and the compiler will issue an error, even if there is a perfect match in base::foo(int)
. Note that this is an error in the call, not an error in the class definition.
When processing the third expression, b.foo( "you" );
the static type of the object is base
(note that the actual object is derived
, but the type of the reference is base&
), so lookup will not search in derived
but rather start in base
. It finds two overloads, and one of them is a good match, so it will call base::foo( const char* )
. The same goes for b.foo(5)
.
Finally, while adding the different overloads in the most derived class hide the overloads in the base, it does not remove them from the objects, so you can actually call the overload that you need by fully qualifying the call (which disables lookup and has the added side effect of skipping dynamic dispatch if the functions were virtual), so d.base::foo( "there" )
will not perform any lookup at all and just dispatch the call to base::foo( const char* )
.
If you had added a using base::foo
declaration to the derived
class, you would add all the overloads of foo
in base
to the available overloads in derived
, and the call d.foo( "Hi" );
would consider the overloads in base
and find that the best overload is base::foo( const char* );
, so it will actually be executed as d.base::foo( "Hi" );
In many cases, developers are not always thinking on how the lookup rules actually work, and it might be surprising that the call to d.foo( 5 );
fails without the using base::foo
declaration, or worse, that the call to d.foo( "Hi" );
is dispatched to derived::foo( std::string const & )
when it is clearly a worse overload than base::foo( const char* )
. That is one of the reasons why compilers warn when you hide member functions. The other good reason for that warning is that in many cases when you actually intended to override a virtual function you might end up mistakenly changing the signature:
struct base {
virtual std::string name() const {
return "base";
};
};
struct derived : base {
virtual std::string name() { // missing const!!!!
return "derived";
}
}
int main() {
derived d;
base & b = d;
std::cout << b.name() << std::endl; // "base" ????
}
A small mistake while trying to override the member function name
(forgetting the const
qualifier) means that you are actually creating a different function signature. derived::name
is not an override to base::name
and thus a call to name
through a reference to base
will not be dispatched to derived::name
!!!
using A::PreCompose;
using A::PostCompose;
makes the errors and warnings vanish, but why is this necessary?
If you add new functions to your derived class with same name as your base class contains and if you don't override the virtual functions from base class, then new names hide the old names from the base class.
That is why you need to unhide them by explicity writing:
using A::PreCompose;
using A::PostCompose;
Other way to unhide them (in this particular case) is , override the virtual functions from base class which you've done in the code you've posted. I believe that code would compile just fine.
Classes are scopes and the look up in a base class is described as looking up in an enclosing scope.
When looking up overload of a function, looking up in enclosing scope is not done if a function was found in the nested one.
A consequence of the two rules is the behaviour you experimented. Adding the using clauses import the definition from the enclosing scope and is the normal solution.