In addition to introducing changes in class constructor rules, MI often requires other coding adjustments. Consider the problem of extending the Show()
method to the SingingWaiter
class. Because a SingingWaiter
object has no new data members, you might think the class could just use the inherited methods. This brings up the first problem. Suppose you do omit a new version of Show()
and try to use a SingingWaiter
object to invoke an inherited Show()
method:
SingingWaiter newhire("Elise Hawks", 2005, 6, soprano);
newhire.Show(); // ambiguous
With single inheritance, failing to redefine Show()
results in using the most recent ancestral definition. In this case, each direct ancestor has a Show()
function, which makes this call ambiguous.
Multiple Inheritance can result in ambiguous function calls. For example, a BadDude
class could inherit two quite different Draw()
methods from a Gunslinger
class and a PokerPlayer
class.
You can use the scope-resolution operator to clarify what you mean:
SingingWaiter newhire("Elise Hawks", 2005, 6, soprano);
newhire.Singer::Show(); // use Singer version
However, a better approach is to redefine Show()
for SingingWaiter
and to have it specify which Show()
to use. For example, if you want a SingingWaiter
object to use the Singer
version, you could use this:
void SingingWaiter::Show()
{
Singer::Show();
}
This method of having the derived method call the base method works well enough for single inheritance. For example, suppose that the HeadWaiter
class derives from the Waiter
class. You could use a sequence of definitions like this, with each derived class adding to the information displayed by its base class:
void Worker::Show() const
{
cout << "Name: " << fullname << "
";
cout << "Employee ID: " << id << "
";
}
void Waiter::Show() const
{
Worker::Show();
cout << "Panache rating: " << panache << "
";
}
void HeadWaiter::Show() const
{
Waiter::Show();
cout << "Presence rating: " << presence << "
";
}
This incremental approach fails for the SingingWaiter
case, however. The following method fails because it ignores the Waiter
component:
void SingingWaiter::Show()
{
Singer::Show();
}
You can remedy that by calling the Waiter
version also:
void SingingWaiter::Show()
{
Singer::Show();
Waiter::Show();
}
However, this displays a person’s name and ID twice because Singer::Show()
and with Waiter::Show()
both call Worker::Show()
.
How can you fix this? One way is to use a modular approach instead of an incremental approach. That is, you can provide a method that displays only Worker
components, another method that displays only Waiter
components (instead of Waiter
plus Worker
components), and another that displays only Singer
components. Then the SingingWaiter::Show()
method can put those components together. For example, you could use this:
void Worker::Data() const
{
cout << "Name: " << fullname << "
";
cout << "Employee ID: " << id << "
";
}
void Waiter::Data() const
{
cout << "Panache rating: " << panache << "
";
}
void Singer::Data() const
{
cout << "Vocal range: " << pv[voice] << "
";
}
void SingingWaiter::Data() const
{
Singer::Data();
Waiter::Data();
}
void SingingWaiter::Show() const
{
cout << "Category: singing waiter
";
Worker::Data();
Data();
}
Similarly, the other Show()
methods would be built from the appropriate Data()
components.
With this approach, objects would still use the Show()
method publicly. The Data()
methods, on the other hand, should be internal to the classes; they should be helper methods used to facilitate the public interface. However, making the Data()
methods private would prevent, say, Waiter
code from using Worker::Data()
. Here is just the kind of situation for which the protected access class is useful. If the Data()
methods are protected, they can by used internally by all the classes in the hierarchy while being kept hidden from the outside world.
Another approach would be to make all the data components protected instead of private, but using protected methods instead of protected data puts tighter control on the allowable access to the data.
The Set()
methods, which solicit data for setting object values, present a similar problem. For example, SingingWaiter::Set()
should ask for Worker
information once, not twice. The same solution used for Show()
works. You can provide protected Get()
methods that solicit information for just a single class, and then you can put together Set()
methods that use the Get()
methods as building blocks.
In short, introducing MI with a shared ancestor requires introducing virtual base classes, altering the rules for constructor initialization lists, and possibly recoding the classes if they were written with MI in mind. Listing 14.10 shows the modified class declarations with these changes institutes, and Listing 14.11 shows the implementation.
// workermi.h -- working classes with MI
#ifndef WORKERMI_H_
#define WORKERMI_H_
#include <string>
class Worker // an abstract base class
{
private:
std::string fullname;
long id;
protected:
virtual void Data() const;
virtual void Get();
public:
Worker() : fullname("no one"), id(0L) {}
Worker(const std::string & s, long n)
: fullname(s), id(n) {}
virtual ~Worker() = 0; // pure virtual function
virtual void Set() = 0;
virtual void Show() const = 0;
};
class Waiter : virtual public Worker
{
private:
int panache;
protected:
void Data() const;
void Get();
public:
Waiter() : Worker(), panache(0) {}
Waiter(const std::string & s, long n, int p = 0)
: Worker(s, n), panache(p) {}
Waiter(const Worker & wk, int p = 0)
: Worker(wk), panache(p) {}
void Set();
void Show() const;
};
class Singer : virtual public Worker
{
protected:
enum {other, alto, contralto, soprano,
bass, baritone, tenor};
enum {Vtypes = 7};
void Data() const;
void Get();
private:
static char *pv[Vtypes]; // string equivs of voice types
int voice;
public:
Singer() : Worker(), voice(other) {}
Singer(const std::string & s, long n, int v = other)
: Worker(s, n), voice(v) {}
Singer(const Worker & wk, int v = other)
: Worker(wk), voice(v) {}
void Set();
void Show() const;
};
// multiple inheritance
class SingingWaiter : public Singer, public Waiter
{
protected:
void Data() const;
void Get();
public:
SingingWaiter() {}
SingingWaiter(const std::string & s, long n, int p = 0,
int v = other)
: Worker(s,n), Waiter(s, n, p), Singer(s, n, v) {}
SingingWaiter(const Worker & wk, int p = 0, int v = other)
: Worker(wk), Waiter(wk,p), Singer(wk,v) {}
SingingWaiter(const Waiter & wt, int v = other)
: Worker(wt),Waiter(wt), Singer(wt,v) {}
SingingWaiter(const Singer & wt, int p = 0)
: Worker(wt),Waiter(wt,p), Singer(wt) {}
void Set();
void Show() const;
};
#endif
// workermi.cpp -- working class methods with MI
#include "workermi.h"
#include <iostream>
using std::cout;
using std::cin;
using std::endl;
// Worker methods
Worker::~Worker() { }
// protected methods
void Worker::Data() const
{
cout << "Name: " << fullname << endl;
cout << "Employee ID: " << id << endl;
}
void Worker::Get()
{
getline(cin, fullname);
cout << "Enter worker's ID: ";
cin >> id;
while (cin.get() != '
')
continue;
}
// Waiter methods
void Waiter::Set()
{
cout << "Enter waiter's name: ";
Worker::Get();
Get();
}
void Waiter::Show() const
{
cout << "Category: waiter
";
Worker::Data();
Data();
}
// protected methods
void Waiter::Data() const
{
cout << "Panache rating: " << panache << endl;
}
void Waiter::Get()
{
cout << "Enter waiter's panache rating: ";
cin >> panache;
while (cin.get() != '
')
continue;
}
// Singer methods
char * Singer::pv[Singer::Vtypes] = {"other", "alto", "contralto",
"soprano", "bass", "baritone", "tenor"};
void Singer::Set()
{
cout << "Enter singer's name: ";
Worker::Get();
Get();
}
void Singer::Show() const
{
cout << "Category: singer
";
Worker::Data();
Data();
}
// protected methods
void Singer::Data() const
{
cout << "Vocal range: " << pv[voice] << endl;
}
void Singer::Get()
{
cout << "Enter number for singer's vocal range:
";
int i;
for (i = 0; i < Vtypes; i++)
{
cout << i << ": " << pv[i] << " ";
if ( i % 4 == 3)
cout << endl;
}
if (i % 4 != 0)
cout << '
';
cin >> voice;
while (cin.get() != '
')
continue;
}
// SingingWaiter methods
void SingingWaiter::Data() const
{
Singer::Data();
Waiter::Data();
}
void SingingWaiter::Get()
{
Waiter::Get();
Singer::Get();
}
void SingingWaiter::Set()
{
cout << "Enter singing waiter's name: ";
Worker::Get();
Get();
}
void SingingWaiter::Show() const
{
cout << "Category: singing waiter
";
Worker::Data();
Data();
}
Of course, curiosity demands that you test these classes, and Listing 14.12 provides code to do so. Note that the program makes use of polymorphism by assigning the addresses of various kinds of classes to base-class pointers. Also the program uses the C-style string
library function strchr()
in the following test:
while (strchr("wstq", choice) == NULL)
This function returns the address of the first occurrence of the choice
character value in the string "wstq"
; the function returns the NULL
pointer if the character isn’t found. This test is simpler to write than an if
statement that compares choice
to each letter individually.
Be sure to compile Listing 14.12 along with workermi.cpp
.
// workmi.cpp -- multiple inheritance
// compile with workermi.cpp
#include <iostream>
#include <cstring>
#include "workermi.h"
const int SIZE = 5;
int main()
{
using std::cin;
using std::cout;
using std::endl;
using std::strchr;
Worker * lolas[SIZE];
int ct;
for (ct = 0; ct < SIZE; ct++)
{
char choice;
cout << "Enter the employee category:
"
<< "w: waiter s: singer "
<< "t: singing waiter q: quit
";
cin >> choice;
while (strchr("wstq", choice) == NULL)
{
cout << "Please enter a w, s, t, or q: ";
cin >> choice;
}
if (choice == 'q')
break;
switch(choice)
{
case 'w': lolas[ct] = new Waiter;
break;
case 's': lolas[ct] = new Singer;
break;
case 't': lolas[ct] = new SingingWaiter;
break;
}
cin.get();
lolas[ct]->Set();
}
cout << "
Here is your staff:
";
int i;
for (i = 0; i < ct; i++)
{
cout << endl;
lolas[i]->Show();
}
for (i = 0; i < ct; i++)
delete lolas[i];
cout << "Bye.
";
return 0;
}
Here is a sample run of the program in Listings 14.10, 14.11, and 14.12:
Enter the employee category:
w: waiter s: singer t: singing waiter q: quit
w
Enter waiter's name: Wally Slipshod
Enter worker's ID: 1040
Enter waiter's panache rating: 4
Enter the employee category:
w: waiter s: singer t: singing waiter q: quit
s
Enter singer's name: Sinclair Parma
Enter worker's ID: 1044
Enter number for singer's vocal range:
0: other 1: alto 2: contralto 3: soprano
4: bass 5: baritone 6: tenor
5
Enter the employee category:
w: waiter s: singer t: singing waiter q: quit
t
Enter singing waiter's name: Natasha Gargalova
Enter worker's ID: 1021
Enter waiter's panache rating: 6
Enter number for singer's vocal range:
0: other 1: alto 2: contralto 3: soprano
4: bass 5: baritone 6: tenor
3
Enter the employee category:
w: waiter s: singer t: singing waiter q: quit
q
Here is your staff:
Category: waiter
Name: Wally Slipshod
Employee ID: 1040
Panache rating: 4
Category: singer
Name: Sinclair Parma
Employee ID: 1044
Vocal range: baritone
Category: singing waiter
Name: Natasha Gargalova
Employee ID: 1021
Vocal range: soprano
Panache rating: 6
Bye.
Let’s look at a few more matters concerning MI.
Let’s consider again the case of a derived class that inherits a base class by more than one route. If the base class is virtual, the derived class contains one subobject of the base class. If the base class is not virtual, the derived class contains multiple subobjects. What if there is a mixture? Suppose, for example, that class B
is a virtual base class to classes C
and D
and a nonvirtual base class to classes X
and Y
. Furthermore, suppose class M
is derived from C
, D
, X
, and Y
. In this case, class M
contains one class B
subobject for all the virtually derived ancestors (that is, classes C
and D
) and a separate class B
subobject for each nonvirtual ancestor (that is, classes X
and Y
). So, all told, it would contain three class B
subobjects. When a class inherits a particular base class through several virtual paths and several nonvirtual paths, the class has one base-class subobject to represent all the virtual paths and a separate base-class subobject to represent each nonvirtual path.
Using virtual base classes alters how C++ resolves ambiguities. With nonvirtual base classes, the rules are simple. If a class inherits two or more members (data or methods) with the same name from different classes, using that name without qualifying it with a class name is ambiguous. If virtual base classes are involved, however, such a use may or may not be ambiguous. In this case, if one name dominates all others, it can be used unambiguously without a qualifier.
So how does one member name dominate another? A name in a derived class dominates the same name in any ancestor class, whether direct or indirect. For example, consider the following definitions:
class B
{
public:
short q();
...
};
class C : virtual public B
{
public:
long q();
int omg()
...
};
class D : public C
{
...
};
class E : virtual public B
{
private:
int omg();
...
};
class F: public D, public E
{
...
};
Here the definition of q()
in class C
dominates the definition in class B
because C
is derived from B
. Thus, methods in F
can use q()
to denote C::q()
. On the other hand, neither definition of omg()
dominates the other because neither C
nor E
is a base class to the other. Therefore, an attempt by F
to use an unqualified omg()
would be ambiguous.
The virtual ambiguity rules pay no attention to access rules. That is, even though E::omg()
is private and hence not directly accessible to class F
, using omg()
is ambiguous. Similarly, even if C::q()
were private, it would dominate D::q()
. In that case, you could call B::q()
in class F
, but an unqualified q()
for that would refer to the inaccessible C::q()
.
3.146.176.145