C++ does multiple inheritance wrong
01 Sep 2020 - John Z. Li
Multiple inheritance could be useful, but C++ does it wrong. To illustrate the problem, Let us consider the following example. Say there are two classes we want to inherit from as below.
class employee {
public:
employee(std::string s, int NO) : name_{s}, no_(NO) {}
virtual void do_something() {
std::cout << "do some employee thing as an employee" << std::endl;
}
std::string get_name() { return name_; }
int )get_NO() { return no_; }
private:
std::string name_;
int no_; // employee number
};
class visitor {
public:
visitor(std::string s, int NO) : name_(s), no_(NO) {}
virtual void do_something() {
std::cout << "do some visitor thing as a visitor" << std::endl;
}
std::string get_name() { return name_; }
int get_NO() { return no_; }
private:
std::string name_;
int no_; // visitor number
};
One thing to notice is that class employee
and class visitor
have
conflicting instance variable names and member functions.
Conflicting names
can be divided into three groups:
- Semantically duplicate names, like the
name_
instance variable, andget_name()
member function. When classemployee
and classvisitor
are multiple inherited, the derived class, denoting the set of people who are both an employee and a visitor, can not have too different names, but only one name, which is supposed to be returned by theget_name()
member function. So it is meaningless to have two (potentially different ) versions of theget_name()
function in the derived class. - Syntactically conflicting names, like the
no_
instance variable and theget_NO()
member function. Despite having the same name, they actually refer to different things. The instance variableno_
in classemployee
means an employee number and that of class visitor means a visitor number, with their correspondingget_NO()
function returning them, respectively. - Finally, the virtual function
do_something()
in two classes is both semantically duplicate and syntactically conflict with each other.
The following should be natural:
- With semantically duplicate names, the reasonable thing to do is to merge them (Common Lisp works this way). It does not make sense that a person who is both an employee and a visitor has two different names. Keeping two copies of the same member variable also does not make sense.
- With syntactically conflicting names, the reasonable thing to do is provide a way to disambiguate them from each other in derived classes, while the ambiguous reference to the original member should become unavalible.
- With conflicting virtual functions, it should be merged but makes it illegal if the derived class does not override it as if the virtual function in bases classes are marked as pure virtual. With some boilerplate code, the latter can be done in C++, by creating proxy classes, as below.
An imaginray syntax achieving all three goals should be look like the below:
(Not Really Legal C++: Imaginary Syntax)
class visitor_empleyee : public employee, public visitor {
public:
visitor_empleyee(std::string name, int em_no, int vi_no)
: employee_proxy(name, em_no), visitor_proxy(_, vi_no) {}
// initialize the member variable name_ only once.
virtual void do_some_something() override {
std::cout << "do some employee thing as both an employee and a vistor"
<< std::endl;
}
// not provide an implementation to override base classes' virtual member
// function do_something() triggers a compilation error.
// std::string get_name() is implicitly emerged, will return the value of
//also implicitly emerged "name_"
using get_employee_NO = employee::get_NO();
// rename the base class employee's member function get_NO to get_employee_NO
using get_visitor_NO = visitor::get_NO();
// rename the base class visitor's member function get_NO to get_visitor_NO
int get_NO() = delete;
// make it illegal to call "get_NO()" from an instance of visitor_employee
private:
// std::string name_ is implicilty emerged into only one.
using emplyee_no_ = emplyee::no_;
// tell the compiler not to merge the member variable no_ in base class employee,
// instead, rename it to employ_no_
using visitor_no = visitor::no_;
// tell the compiler not to merge the member variable no_ in class visitor,
// instead, rename it to "visitor_no_"
};
With this imaginary syntax, the language will have the following semantics:
employee* ep = new visitor_employee("John", 217, 336);
visitor* vp = new visitor_employee("Peter", 218, 337);
ep.get_name(); //OK: returns "John"
vp.get_name(); //OK: returns "Peter"
ep.get_NO(); // OK: calls employee::get_NO();
vp.get_NO(); // OK: calls visitor::get_NO();
dynamic_cast<visitor_employee*>(ep).get_NO(); // compilation error, ambiguous.
dynamic_cast<visitor_employee*>(vp).get_NO(); // Error, as above
dynamic_cast<visitor_employee*>(ep).get_name(); //OK, returns "John"
dynamic_cast<visitor_employee*>(vp).get_name(); //OK, returns "Peter"
dynamic_cast<visitor_employee*>(ep).get_employee_NO(); //OK: returns 217
dynamic_cast<visitor_employee*>(ep).get_visitor_NO(); //OK: returns 336
dynamic_cast<visitor_employee*>(vp).get_employee_NO(); //OK: returns 218
dynamic_cast<visitor_employee*>(vp).get_visitor_NO(); //OK: returns 337
ep.do_something(); //OK: calls visitor_employee::do_something();
vp.do_something(); //OK: calls visitor_employee::do_something();
// true
assert(sizeof(visitor_employee) < sizeof(visitor) + sizeof(employee));
I hope someday a new programming language will do multiple inheritance correct. It is a little bit too late for C++.