Inapoi
Inainte
Cuprins
Choosing composition vs.
inheritance
Both composition and inheritance place
subobjects
inside your new class. Both use the constructor initializer list to construct
these subobjects. You may now be wondering what the difference is between the
two, and when to choose one over the other.
Composition is generally used when you
want the features of an existing class inside your new class, but not its
interface. That is, you embed an object to implement features of your new class,
but the user of your new class sees the interface you’ve defined rather
than the interface from the original class. To do this, you follow the typical
path of embedding private objects of existing classes inside your new
class.
Occasionally, however, it makes sense to
allow the class user to directly access the composition of your new class, that
is, to make the member objects public. The member objects use access
control themselves, so this is a safe thing to do and when the user knows
you’re assembling a bunch of parts, it makes the interface easier to
understand. A Car class is a good example:
//: C14:Car.cpp
// Public composition
class Engine {
public:
void start() const {}
void rev() const {}
void stop() const {}
};
class Wheel {
public:
void inflate(int psi) const {}
};
class Window {
public:
void rollup() const {}
void rolldown() const {}
};
class Door {
public:
Window window;
void open() const {}
void close() const {}
};
class Car {
public:
Engine engine;
Wheel wheel[4];
Door left, right; // 2-door
};
int main() {
Car car;
car.left.window.rollup();
car.wheel[0].inflate(72);
} ///:~
Because the composition of a Car
is part of the analysis of the problem (and not simply part of the underlying
design), making the members public assists the client programmer’s
understanding of how to use the class and requires less code complexity for the
creator of the class.
With a little thought, you’ll also
see that it would make no sense to compose a Car using a
“vehicle” object – a car doesn’t contain a vehicle, it
is a vehicle. The is-a relationship is expressed with inheritance,
and the has-a relationship is expressed with
composition.
Subtyping
Now suppose you want to create a type of
ifstream object that not only opens a file but
also keeps track of the name of the file. You can use composition and embed both
an ifstream and a string into the new class:
//: C14:FName1.cpp
// An fstream with a file name
#include "../require.h"
#include <iostream>
#include <fstream>
#include <string>
using namespace std;
class FName1 {
ifstream file;
string fileName;
bool named;
public:
FName1() : named(false) {}
FName1(const string& fname)
: fileName(fname), file(fname.c_str()) {
assure(file, fileName);
named = true;
}
string name() const { return fileName; }
void name(const string& newName) {
if(named) return; // Don't overwrite
fileName = newName;
named = true;
}
operator ifstream&() { return file; }
};
int main() {
FName1 file("FName1.cpp");
cout << file.name() << endl;
// Error: close() not a member:
//! file.close();
} ///:~
There’s a problem here, however. An
attempt is made to allow the use of the FName1 object anywhere an
ifstream object is used by including an automatic type conversion
operator from FName1 to an ifstream&. But in main, the
line
file.close();
will not compile because automatic type
conversion happens only in function calls, not during member selection. So this
approach won’t work.
A second approach is to add the
definition of close( ) to FName1:
void close() { file.close(); }
This will work if there are only a few
functions you want to bring through from the ifstream class. In that case
you’re only using part of the class, and composition
is appropriate.
But what if you want everything in the
class to come through? This is called subtyping because you’re
making a new type from an existing type, and you want your new type to have
exactly the same interface as the existing type (plus any other member functions
you want to add), so you can use it everywhere you’d use the existing
type. This is where inheritance is essential. You can see that subtyping solves
the problem in the preceding example perfectly:
//: C14:FName2.cpp
// Subtyping solves the problem
#include "../require.h"
#include <iostream>
#include <fstream>
#include <string>
using namespace std;
class FName2 : public ifstream {
string fileName;
bool named;
public:
FName2() : named(false) {}
FName2(const string& fname)
: ifstream(fname.c_str()), fileName(fname) {
assure(*this, fileName);
named = true;
}
string name() const { return fileName; }
void name(const string& newName) {
if(named) return; // Don't overwrite
fileName = newName;
named = true;
}
};
int main() {
FName2 file("FName2.cpp");
assure(file, "FName2.cpp");
cout << "name: " << file.name() << endl;
string s;
getline(file, s); // These work too!
file.seekg(-200, ios::end);
file.close();
} ///:~
Now any member function available for an
ifstream object is available for an FName2 object. You can also
see that non-member functions like getline( ) that expect an
ifstream can also work with an FName2. That’s because
an FName2 is a type of ifstream; it doesn’t simply
contain one. This is a very important issue that will be explored at the end of
this chapter and in the next
one.
private inheritance
You can inherit a base class privately by
leaving off the public in the base-class list, or by explicitly saying
private (probably a better policy because it is clear to the user that
you mean it). When you inherit privately, you’re “implementing in
terms of;” that is, you’re creating a new class that has all of the
data and functionality of the base class, but that functionality is hidden, so
it’s only part of the underlying implementation. The class user has no
access to the underlying functionality, and an object cannot be treated as a
instance of the base class (as it was in FName2.cpp).
You may wonder what the purpose of
private inheritance is, because the alternative of using composition to
create a private object in the new class seems more appropriate.
private inheritance is included in the language for completeness, but if
for no other reason than to reduce confusion, you’ll usually want to use
composition rather than private inheritance. However, there may
occasionally be situations where you want to produce part of the same interface
as the base class and disallow the treatment of the object as if it were
a base-class object. private inheritance provides this
ability.
Publicizing privately inherited members
When you inherit privately, all the
public members of the base class become private. If you want any
of them to be visible, just say their names (no arguments or return values) in
the public section of the derived class:
//: C14:PrivateInheritance.cpp
class Pet {
public:
char eat() const { return 'a'; }
int speak() const { return 2; }
float sleep() const { return 3.0; }
float sleep(int) const { return 4.0; }
};
class Goldfish : Pet { // Private inheritance
public:
Pet::eat; // Name publicizes member
Pet::sleep; // Both overloaded members exposed
};
int main() {
Goldfish bob;
bob.eat();
bob.sleep();
bob.sleep(1);
//! bob.speak();// Error: private member function
} ///:~
Thus, private inheritance is
useful if you want to hide part of the functionality of the base
class.
Notice that giving the name of an
overloaded function exposes all the versions of the overloaded function in the
base class.
You should think carefully before using
private inheritance instead of composition; private inheritance
has particular complications when combined with runtime type identification
(this is the topic of a chapter in Volume 2 of this book, downloadable from
www.BruceEckel.com).
protected
Now that you’ve been introduced to
inheritance, the keyword
protected finally has
meaning. In an ideal world,
private members would
always be hard-and-fast private, but in real projects there are times
when you want to make something hidden from the world at large and yet allow
access for members of derived classes. The protected keyword is a nod to
pragmatism; it says, “This is private as far as the class user is
concerned, but available to anyone who inherits from this
class.”
The best approach is to leave the data
members private – you should always preserve your right to change
the underlying implementation. You can then allow controlled access to
inheritors of your class through protected member
functions:
//: C14:Protected.cpp
// The protected keyword
#include <fstream>
using namespace std;
class Base {
int i;
protected:
int read() const { return i; }
void set(int ii) { i = ii; }
public:
Base(int ii = 0) : i(ii) {}
int value(int m) const { return m*i; }
};
class Derived : public Base {
int j;
public:
Derived(int jj = 0) : j(jj) {}
void change(int x) { set(x); }
};
int main() {
Derived d;
d.change(10);
} ///:~
You will find examples of the need for
protected in examples later in this book, and in Volume
2.
protected inheritance
When you’re inheriting, the base
class defaults to private, which means that all of the public member
functions are private to the user of the new class. Normally,
you’ll make the inheritance public so the interface of the base
class is also the interface of the derived class. However, you can also use the
protected keyword during inheritance.
Protected derivation means
“implemented-in-terms-of” to other classes but “is-a”
for derived classes and friends. It’s something you don’t use very
often, but it’s in the language for
completeness.
 |
|