I have a simple c++ base class and two derived classes:
class base;
class der1;
class der2;
class base{
public:
virtual void act(base*); // ignore for now
virtual void print(){
cout << "base" << endl;
}
};
class der1: public base{
public:
void act(der1*); // ignore for now
void act(der2*); // ignore for now
void print(){
cout << "der1" << endl;
}
};
class der2: public base{
public:
void act(der1*); // ignore for now
void act(der2*); // ignore for now
void print(){
cout << "der2" << endl;
}
};
I now create an instance of each derived class, and call the print
methods thereon:
der1 d1;
der2 d2;
d1.print(); // prints "der1" as expected
d2.print(); // prints "der2" as expected
Now I assemble these two objects into an array of base
pointers, and again call the print
method:
base ** B;
B = new base*[2];
B[0] = &d1;
B[1] = &d2;
B[0]->print(); // again, prints "der1" as expected
B[1]->print(); // again, prints "der2" as expected
So far so good: gathering the two objects together did not make them forget their derived class. B
knows that its objects are der1
and der2
classes, it did not call base::print()
. Now comes the problem: I also need methods that take objects as inputs, again remembering their derived class:
void base::act(base * x){
cout << "base act on base" << endl;
}
void der1::act(der1 * x){
cout << "der1 act on der1" << endl;
}
void der1::act(der2 * x){
cout << "der1 act on der2" << endl;
}
void der2::act(der1 * x){
cout << "der2 act on der1" << endl;
}
void der2::act(der2 * x){
cout << "der2 act on der2" << endl;
}
Now, if I call these methods on d1
, d2
directly, it behaves as expected:
d1.act(&d1); // prints "der1 act on der1"
d1.act(&d2); // prints "der1 act on der2"
d2.act(&d1); // prints "der2 act on der1"
d2.act(&d2); // prints "der2 act on der2"
but if I call them on the array, it resorts back into the base method:
B[0]->act(B[0]); // prints "base act on base"
B[0]->act(B[1]); // prints "base act on base"
B[1]->act(B[0]); // prints "base act on base"
B[1]->act(B[1]); // prints "base act on base"
Is there any way around this? I want the array B
to behave like the respective objects, which works fine for the print
method (that takes no input), but fails for the act
method (which does take an input).
You should try adding override
on all overriding methods in your derived classes, you'll have a good insight on the problem.
Try it and discover for yourself what is going wrong.
Now if you just want to skip to the answer, here it is:
der1
and der2
are not overriding the base's act
. This is because there is no method act
taking a base*
in your derived class. As this is the method you're supposed to override, you need to define one with exactly the right signature. To make sure you got it right, always use override
:
class base{
public:
virtual void act(base*);
virtual void print(){
cout << "base" << endl;
}
};
class der1: public base{
public:
void act(base*) override; // does override
void act(der1*); // not overriding anything
void act(der2*); // not overriding anything
void print(){
cout << "der1" << endl;
}
};
class der2: public base{
public:
void act(base*) override; // does override
void act(der1*); // not overriding anything
void act(der2*); // not overriding anything
void print(){
cout << "der2" << endl;
}
};
So act(base*)
is virtual, whereas void act(der1*)
and void act(der2*)
are not virtual. They are completely different methods.
Does that mean you cannot write a method that matches the type of multiple classes?
Indeed. C++ has no multimethod support. You'll need to declare a method for each derived class in the base class. However, if you choose to do so, you will loose something very important about inheritance based polymorphism: You can no longer have an unbounded amount of types!
See this table:
+--------------------+-----------------------+
| amount of types | amount of operations |
+-------------+--------------------+-----------------------+
| inheritance | unbounded | bounded by base class |
+-------------+--------------------+-----------------------+
| variant | bounded by variant | unbounded |
+-------------+--------------------+-----------------------+
When designing a inheritance based polymorphism, all code knowing there is polymorphism at play should only know and deal in term of base*
.
You cannot have both an open amount of operations and an open amount of types. If you add a visitor with a set of type combinations, you loose the ability for polymorphism to be unbounded in types, and you can only have an unbounded amount of operation through double dispatch. Using a variant is generally less convoluted as they naturally work with visitors and pattern matching.
Given variants, your problem is easy to solve:
// define this helper somewhere
template<class... Ts>
struct overloaded : Ts... { using Ts::operator()...; };
std::vector<std::variant<der1, der2>> B;
B.emplace_back(der1{});
B.emplace_back(der2{});
std::visit(overloaded{
[](der1 const&, der1 const&) { std::println("der1 act on der1"); },
[](der1 const&, der2 const&) { std::println("der1 act on der2"); },
[](der2 const&, der1 const&) { std::println("der2 act on der1"); },
[](der2 const&, der2 const&) { std::println("der2 act on der2"); },
}, B[0], B[1]);
You might notice you don't need the base class at all using a variant. No chance of slicing either and no pointers.
If you want to keep the inheritance instead, then I would either recommend not depending on the dynamic type of two objects, or doing double dispatch using the visitor pattern.