Topic: pure and impure MI
Author: cok@islsun.Kodak.COM (David Cok)
Date: Fri, 31 Jan 92 21:10:30 GMT Raw View
Consider the following program in which the function g is inherited
through both branches of a multiple inheritance strcture.
class A {
public:
virtual int g() = 0;
};
class B {
int i;
public:
virtual int g() { return i; }
};
class C: public A, public B {
};
class D: public C {
public:
virtual int g() { return B::g(); } // Why is this needed?
};
main()
{
D d;
d.g();
A* a = &d; B* b = &d;
a->g();
b->g();
}
My reading of the ARM is that the definition of g in class D is
required. Indeed, Sun C++ 2.1 complains both that D is an abstract
class and that g is ambiguous if that definition is omitted:
"test.C", line 22: error: declaration of object of abstract class D
"test.C", line 23: error: ambiguous A::g() and B::g()
I'd like to argue that this should not be so; with the definition of
g omitted, the default behavior ought to be to use the one defined
function (B::g) as the definition of C::g and D::g.
If both A::g and B::g are pure functions, then obviously g needs to
be defined somewhere. If both A::g and B::g are non-pure functions
then obviously there is ambiguity which must be resolved if g is going
to be used. However, if among all the base classes there is exactly one
non-pure definition of a function and any number of pure definitions, then
it seems reasonable that the one definition such as the one for D::g
above should be automatically presumed by the compiler.
There are two design situations in which I find MI to be helpful. The
first is commonly termed mixins. One has a class hierarchy with some
pure virtual functions and at some level one has to supply a definition
of that function. If one has an implementation in another class, a
convenient way to use that implementation in a derived class without
having to modify or even have access to the source code of the
implementation is through multiple inheritance. Having to supply the
extra definition in the derived class (D::g above) is stating the
obvious.
A second situation is where the leaf classes are essentially the outer
product of two sets of functionality. I have a base class Z with
derived classes A, B, C, D; for each of these I need two versions, 1 and
2, giving classes A1 A2 B1 B2 C1 C2 D1 D2. These classes all have the
same functionality but implement it in different ways; the implementations
in the 1 class can be put into a common class X1 and the implementations
in the 2 classes can be put into a common class X2. So we have something
like
class Z { public: void eval() = 0; void set_value(double d)= 0; };
class A: public Z { // int version
int i; public: void set_value(double d) { i = (int)d;} };
class B: public Z { // float version
float f; public: void set_value(double d) { f = (float)d;} };
class X1 { void eval() { ... chained evaluation ... }};
class X2 { void eval() { ... non-chained evaluation ... }};
/* Text with current rules */
class A1: public A, public X1 { void eval() { X1::eval(); }}; // chained int version
class A2: public A, public X2 { void eval() { X2::eval(); }}; // nonchained int version
class B1: public B, public X1 { void eval() { X1::eval(); }}; // chained float version
... and so on
/* Text with proposed change */
class A1: public A, public X1 {}; // chained int version
class A2: public A, public X2 {}; // nonchained int version
class B1: public B, public X1 {}; // chained float version
... and so on
This is a convenient way of keeping common functionality in one place.
Having to put all the extra definitions in each of A1... is a verbose
pain, especially when functions are added or removed from Z, X1, and X2.
This rule change would replace what is now an error condition with a default
behavior. The implementation cost is nil -- or perhaps even negative
since (if this is the common case) it saves the compiler from having to
figure out that all the user is doing is calling the implementation in
one of the base classes. It also fits in with the general rule that
when you don't redefine a function, you inherit from the base classes.
The only possible drawback is that the user would not be warned if two
function names were inadvertently the same
(but that can happen all the time with variable names and functions with
different signatures). Even if you don't like MI, given that C++ has
MI the rule change makes MI more usable.
Any comments?
David R. Cok
Image Processing Algorithm Lab
Eastman Kodak Company
cok@Kodak.COM