mirror of
https://gitlab.com/fbb-git/cppannotations
synced 2024-11-16 07:48:44 +01:00
WIP
git-svn-id: https://cppannotations.svn.sourceforge.net/svnroot/cppannotations/trunk@329 f6dd340e-d3f9-0310-b409-bdd246841980
This commit is contained in:
parent
d14713f960
commit
bced62d86c
7 changed files with 308 additions and 276 deletions
|
@ -1,52 +1,46 @@
|
|||
When the operator tt(delete) releases memory occupied by a dynamically
|
||||
allocated object, or when an object goes i(out of scope), the appropriate
|
||||
i(destructor) is called to ensure that memory allocated by the object is also
|
||||
deleted. Now consider the following code fragment (cf. section
|
||||
ref(VehicleSystem)):
|
||||
When an object ceases to exist the object's i(destructor) is called. Now
|
||||
consider the following code fragment (cf. section ref(VehicleSystem)):
|
||||
verb(
|
||||
Vehicle *vp = new Land(1000, 120);
|
||||
|
||||
delete vp; // object destroyed
|
||||
)
|
||||
In this example an object of a derived class (tt(Land)) is destroyed using
|
||||
a base class pointer (tt(Vehicle *)). For a `standard' class definition this
|
||||
will mean that tt(Vehicle)'s destructor is called, instead of the tt(Land)
|
||||
object's destructor. This not only results in a i(memory leak) when memory is
|
||||
allocated in tt(Land), but it will also prevent any other task, normally
|
||||
performed by the derived class's destructor from being completed (or, better:
|
||||
started). A Bad Thing.
|
||||
Here tt(delete) is applied to a base class pointer. As the base class
|
||||
defines the available interface tt(delete vp) calls tt(~Vehicle) and tt(~Land)
|
||||
remains out of sight. Assuming that tt(Land) allocates memory a
|
||||
i(memory leak) results. Freeing memory is not the only action destructors can
|
||||
perform. In general they may perform any action that's necessary when an
|
||||
object ceases to exist. But here none of the actions defined by tt(~Land) will
|
||||
be performed. Bad news....
|
||||
|
||||
In bf(C++) this problem is solved using hi(virtual destructor) em(virtual
|
||||
destructors). By applying the keyword tt(virtual) to the declaration of a
|
||||
i(destructor) the appropriate derived class destructor is activated when the
|
||||
argument of the ti(delete) operator is a i(base class pointer). In the
|
||||
following partial class definition the declaration of such a virtual
|
||||
destructor is shown:
|
||||
In bf(C++) this problem is solved by
|
||||
hi(destructor: virtual)hi(virtual destructor) em(virtual destructors). A
|
||||
destructor can be declared tt(virtual). When a base class destructor is
|
||||
declared virtual the destructor of the actual class pointed to by a base class
|
||||
pointer tt(bp) will be called when executing tt(delete bp). Thus, late binding
|
||||
is realized for destructors even though the destructors of derived classes
|
||||
have unique names. Example:
|
||||
verb(
|
||||
class Vehicle
|
||||
{
|
||||
public:
|
||||
virtual ~Vehicle();
|
||||
virtual size_t weight() const;
|
||||
virtual ~Vehicle(); // all derived class destructors are
|
||||
// now virtual as well.
|
||||
};
|
||||
)
|
||||
By declaring a virtual destructor, the above tt(delete) operation
|
||||
(tt(delete vp)) will correctly call tt(Land)'s destructor, rather than
|
||||
tt(Vehicle)'s destructor.
|
||||
tt(Vehicle)'s destructor.
|
||||
|
||||
From this discussion we are now able to formulate the following situations
|
||||
in which a hi(destructor: when to define) destructor should be defined:
|
||||
itemization(
|
||||
it() A destructor should be defined when memory is allocated and managed
|
||||
by objects of the class.
|
||||
it() This destructor should be defined as a em(virtual) destructor if the
|
||||
class contains at least one virtual member function, to prevent incomplete
|
||||
destruction of derived class objects when destroying objects using base class
|
||||
pointers or references pointing to derived class objects (see the initial
|
||||
paragraphs of this section)
|
||||
)
|
||||
In the second case, the destructor doesn't have any special tasks to
|
||||
perform. In these cases the virtual
|
||||
Once a destructor is called it will perform as usual, whether or not it
|
||||
is a virtual destructor. So, tt(~Land) will first execute its own statements
|
||||
and will then call tt(~Vehile). Thus, the above tt(delete vp) statement will
|
||||
use late binding to call tt(~Vehicle) and from this point on the object
|
||||
destruction proceeds as usual.
|
||||
|
||||
Destructors should always be defined tt(virtual) in classes designed as a
|
||||
base class from which other classes are going to be derived. Often those
|
||||
destructors themselves have no tasks to perform. In these cases the virtual
|
||||
hi(empty destructor) hi(destructor: empty)
|
||||
destructor is given an empty body. For example, the definition of
|
||||
tt(Vehicle::~Vehicle()) may be as simple as:
|
||||
|
@ -54,5 +48,13 @@ tt(Vehicle::~Vehicle()) may be as simple as:
|
|||
Vehicle::~Vehicle()
|
||||
{}
|
||||
)
|
||||
Often the destructor will be defined i(inline) below the
|
||||
hi(destructor: inline) i(class interface).
|
||||
Resist the temptation to define destructors (even empty destructors)
|
||||
hi(destructor: inline) i(inline) as this will complicate class
|
||||
maintenance. Section ref(howpolymorphism) discusses the reason behind this
|
||||
i(rule of thumb).
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
|
|
@ -1,48 +0,0 @@
|
|||
#include <iostream>
|
||||
|
||||
class Base
|
||||
{
|
||||
public:
|
||||
virtual ~Base();
|
||||
virtual void pure() = 0;
|
||||
};
|
||||
|
||||
inline Base::~Base()
|
||||
{}
|
||||
|
||||
inline void Base::pure()
|
||||
{
|
||||
std::cout << "Base::pure() called\n";
|
||||
}
|
||||
|
||||
class Derived: public Base
|
||||
{
|
||||
public:
|
||||
virtual void pure();
|
||||
};
|
||||
|
||||
inline void Derived::pure()
|
||||
{
|
||||
Base::pure();
|
||||
std::cout << "Derived::pure() called\n";
|
||||
}
|
||||
|
||||
int main()
|
||||
{
|
||||
Derived derived;
|
||||
|
||||
derived.pure();
|
||||
derived.Base::pure();
|
||||
|
||||
Derived *dp = &derived;
|
||||
|
||||
dp->pure();
|
||||
dp->Base::pure();
|
||||
}
|
||||
// Output:
|
||||
// Base::pure() called
|
||||
// Derived::pure() called
|
||||
// Base::pure() called
|
||||
// Base::pure() called
|
||||
// Derived::pure() called
|
||||
// Base::pure() called
|
43
yo/polymorphism/examples/purevirtualimp.cc
Normal file
43
yo/polymorphism/examples/purevirtualimp.cc
Normal file
|
@ -0,0 +1,43 @@
|
|||
#include <iostream>
|
||||
|
||||
class Base
|
||||
{
|
||||
public:
|
||||
virtual ~Base();
|
||||
virtual void pureimp() = 0;
|
||||
};
|
||||
Base::~Base()
|
||||
{}
|
||||
void Base::pureimp()
|
||||
{
|
||||
std::cout << "Base::pureimp() called\n";
|
||||
}
|
||||
class Derived: public Base
|
||||
{
|
||||
public:
|
||||
virtual void pureimp();
|
||||
};
|
||||
inline void Derived::pureimp()
|
||||
{
|
||||
Base::pureimp();
|
||||
std::cout << "Derived::pureimp() called\n";
|
||||
}
|
||||
int main()
|
||||
{
|
||||
Derived derived;
|
||||
|
||||
derived.pureimp();
|
||||
derived.Base::pureimp();
|
||||
|
||||
Derived *dp = &derived;
|
||||
|
||||
dp->pureimp();
|
||||
dp->Base::pureimp();
|
||||
}
|
||||
// Output:
|
||||
// Base::pureimp() called
|
||||
// Derived::pureimp() called
|
||||
// Base::pureimp() called
|
||||
// Base::pureimp() called
|
||||
// Derived::pureimp() called
|
||||
// Base::pureimp() called
|
|
@ -1,65 +1,59 @@
|
|||
The default behavior of the activation of a member function via a pointer or
|
||||
i(reference) is that the type of the pointer (or reference) determines the
|
||||
function that is called. E.g., a tt(Vehicle *) will activate tt(Vehicle)'s
|
||||
member functions, even when pointing to an object of a derived class. As noted
|
||||
in this chapter's introduction, this is
|
||||
referred to as em(early) or
|
||||
hi(early binding) hi(static binding)
|
||||
em(static) binding, since the type of function is known
|
||||
i(compile-time). The em(late) hi(late bining) or hi(dynamic binding)
|
||||
em(dynamic) binding is achieved in bf(C++) using em(virtual member functions).
|
||||
By default the behavior of a member function called via a pointer or reference
|
||||
is determined by the implementation of that function in the pointer's or
|
||||
reference's class. E.g., a tt(Vehicle *) will activate tt(Vehicle)'s member
|
||||
functions, even when pointing to an object of a derived class. This is known
|
||||
as as em(early) or
|
||||
hi(early binding) hi(static binding) em(static) binding: the function to
|
||||
call is determinded
|
||||
i(compile-time). In bf(C++) em(late)
|
||||
hi(late bining) or hi(dynamic binding) em(dynamic) binding is realized using
|
||||
em(virtual member functions).
|
||||
|
||||
A member function becomes a i(virtual member function) when its declaration
|
||||
starts with the keyword ti(virtual). So once again note that in bf(C++),
|
||||
different from many other object oriented languages, this is em(not) the
|
||||
default situation. By default em(static) binding is used.
|
||||
starts with the keyword ti(virtual). It is stressed once again that in
|
||||
bf(C++), different from several other object oriented languages, this is
|
||||
em(not) the default situation. By default em(static) binding is used.
|
||||
|
||||
Once a function is declared tt(virtual) in a i(base class), it remains a
|
||||
virtual member function in all derived classes; even when the keyword
|
||||
tt(virtual) is not repeated in a
|
||||
i(derived class).
|
||||
Once a function is declared tt(virtual) in a base class, it remains virtual in
|
||||
all derived classes; even when the keyword tt(virtual) is not repeated in
|
||||
derived classes.
|
||||
|
||||
As far as the vehicle classification system is concerned (see section
|
||||
ref(VehicleSystem)) the two member functions tt(weight()) and
|
||||
tt(setWeight()) might well be declared tt(virtual). The relevant sections of
|
||||
the class definitions of the class tt(Vehicle) and tt(Truck) are shown
|
||||
below. Also, we show the implementations of the member functions
|
||||
tt(weight()) of the two classes:
|
||||
In the vehicle classification system (see section ref(VehicleSystem)) the two
|
||||
member functions tt(weight) and tt(setWeight) might be declared
|
||||
tt(virtual). Concentrating on tt(weight) The relevant sections of the class
|
||||
definitions of the class tt(Vehicle) and tt(Truck) are shown below. Also, we
|
||||
show the implementations of the member function tt(weight):
|
||||
verb(
|
||||
class Vehicle
|
||||
{
|
||||
public:
|
||||
virtual int weight() const;
|
||||
virtual void setWeight(int wt);
|
||||
};
|
||||
|
||||
class Truck: public Vehicle
|
||||
class Truck: // inherited from Vehicle through Auto and Land
|
||||
{
|
||||
public:
|
||||
void setWeight(int engine_wt, int trailer_wt);
|
||||
int weight() const;
|
||||
// not altered
|
||||
};
|
||||
|
||||
int Vehicle::weight() const
|
||||
{
|
||||
return (weight);
|
||||
return d_weight;
|
||||
}
|
||||
|
||||
int Truck::weight() const
|
||||
{
|
||||
return (Auto::weight() + trailer_wt);
|
||||
return Auto::weight() + d_trailer_wt;
|
||||
}
|
||||
)
|
||||
Note that the keyword tt(virtual) em(only) needs to appear in the
|
||||
tt(Vehicle) base class. There is no need (but there is also no i(penalty)) to
|
||||
repeat it in derived classes: once tt(virtual), always tt(virtual). On the
|
||||
other hand, a function may be declared tt(virtual) em(anywhere) in a
|
||||
i(class hierarchy): the compiler will be perfectly happy if tt(weight())
|
||||
is declared tt(virtual) in tt(Auto), rather than in tt(Vehicle). The specific
|
||||
characteristics of virtual member functions would then, for the member
|
||||
function tt(weight()), only appear with tt(Auto) (and its derived classes)
|
||||
pointers or references. With a tt(Vehicle) pointer, i(static binding) would
|
||||
remain to be used. The effect of i(late binding) is illustrated below:
|
||||
The keyword tt(virtual) em(only) appears in the (tt(Vehicle)) base
|
||||
class. There is no need (but there is also no i(penalty)) to repeat it in
|
||||
derived classes. Once a class member has been declared tt(virtual) is will be
|
||||
tt(virtual) in all derived classes. A member function may be
|
||||
declared tt(virtual) em(anywhere) in a
|
||||
i(class hierarchy). The compiler will be perfectly happy if tt(weight) is
|
||||
declared tt(virtual) in tt(Auto), rather than in tt(Vehicle). The specific
|
||||
characteristics of virtual member functions would then only be available for
|
||||
tt(Auto) objects and for objects of classes derived from tt(Auto). For a
|
||||
tt(Vehicle) pointer static binding would remain to be used. The effect of
|
||||
late binding is illustrated below:
|
||||
verb(
|
||||
Vehicle v(1200); // vehicle with weight 1200
|
||||
Truck t(6000, 115, // truck with cabin weight 6000, speed 115,
|
||||
|
@ -77,23 +71,17 @@ remain to be used. The effect of i(late binding) is illustrated below:
|
|||
cout << vp->speed() << endl; // see (3) below
|
||||
}
|
||||
)
|
||||
Since the function tt(weight()) is defined tt(virtual), i(late binding)
|
||||
is used:
|
||||
Now that tt(weight) is defined tt(virtual), late binding will be used:
|
||||
itemization(
|
||||
it() at (1), tt(Vehicle::weight()) is called.
|
||||
it() at (2) tt(Truck::weight()) is called.
|
||||
it() at (1), tt(Vehicle::weight) is called.
|
||||
it() at (2) tt(Truck::weight) is called.
|
||||
it() at (3) a syntax error is generated. The member
|
||||
tt(speed()) is no member of tt(Vehicle), and hence not callable via
|
||||
tt(speed) is no member of tt(Vehicle), and hence not callable via
|
||||
a tt(Vehicle*).
|
||||
)
|
||||
The example illustrates that hi(callable member functions)
|
||||
hi(member functions: callable) when a pointer to a class is used
|
||||
em(only the functions which are members of that class can be called). These
|
||||
functions em(may) be tt(virtual). However, this only influences the type of
|
||||
binding (early vs. late) and not the set of member functions that is visible
|
||||
The example illustrates that when a pointer to a class is used em(only the
|
||||
members of that class can be called). These functions may or may not be
|
||||
tt(virtual). A member's tt(virtual) characteristic only influences the type of
|
||||
binding (early vs. late), not the set of member functions that is visible
|
||||
to the pointer.
|
||||
|
||||
A virtual member function cannot be a i(static member function): a virtual
|
||||
member function is still an ordinary member function in that it has a ti(this)
|
||||
pointer. As static member functions have no tt(this) pointer, they cannot be
|
||||
declared virtual.
|
||||
|
|
|
@ -1,27 +1,21 @@
|
|||
Pure virtual member functions may be implemented. To implement a pure virtual
|
||||
i(member function: pure virtual and implemented)
|
||||
hi(implementing pure virtual member functions)
|
||||
hi(pure virtual functions: implementing)
|
||||
i(member function: pure virtual implementation)
|
||||
hi(pure virtual member: implementation)
|
||||
member function, provide it with its normal tt(= 0;) specification, but
|
||||
implement it nonetheless. Since the tt(= 0;) ends in a semicolon, the pure
|
||||
virtual member is always at most a declaration in its class, but an
|
||||
implementation may either be provided in-line below the class interface or it
|
||||
may be defined as a non-inline member function in a source file of its own.
|
||||
implement it as well. Since the tt(= 0;) ends in a semicolon, the pure virtual
|
||||
member is always at most a declaration in its class, but an implementation may
|
||||
either be provided outside from its interface (maybe using tt(inline)).
|
||||
|
||||
Pure virtual member functions may be called from derived class objects or
|
||||
from its class or derived class members by specifying the base class and scope
|
||||
resolution operator with the function to be called. The following small
|
||||
program shows some examples:
|
||||
resolution operator together with the member to be called. Example:
|
||||
|
||||
verbinclude(polymorphism/examples/purevirtual.cc)
|
||||
verbinclude(polymorphism/examples/purevirtualimp.cc)
|
||||
|
||||
Implementing a pure virtual function has limited use. One could argue that
|
||||
the pure virtual function's implementation may be used to perform tasks that
|
||||
can already be performed at the base-class level. However, there is no
|
||||
guarantee that the base class virtual function will actually be called from
|
||||
the derived class overridden version of the member function (like
|
||||
a base class constructor that is automatically called from a derived class
|
||||
constructor). Since the base class implementation will therefore at most be
|
||||
called optionally its functionality could as well be implemented in a separate
|
||||
member, which can then be called without the requirement to mention the base
|
||||
class explicitly.
|
||||
Implementing a pure virtual member has limited use. One could argue that
|
||||
the pure virtual member function's implementation may be used to perform tasks
|
||||
that can already be performed at the base class level. However, there is no
|
||||
guarantee that the base class virtual member function will actually be called.
|
||||
Therefore a base class specific tasks could as well be offered by a separate
|
||||
member, without blurring the distinction between a member doing some work and
|
||||
a pure virtual member enforcing a protocol.
|
||||
|
|
|
@ -1,65 +1,116 @@
|
|||
As we have seen in chapter ref(INHERITANCE), bf(C++) provides the tools to
|
||||
derive classes from base classes, and to use base class pointers to address
|
||||
derived objects. As we've also seen, when using a i(base class pointer) to
|
||||
address an object of a i(derived class), the type of the pointer determines
|
||||
which i(member function) will be used. This means that a tt(Vehicle *vp),
|
||||
pointing to a tt(Truck) object, will incorrectly compute the truck's combined
|
||||
weight in a statement like tt(vp->weight()). The reason for this should now be
|
||||
clear: tt(vp) calls tt(Vehicle::weight()) and not tt(Truck::weight()), even
|
||||
though tt(vp) actually points to a tt(Truck).
|
||||
Using inheritance classes may be derived from other classes, called base
|
||||
classes. In the previous chapter we saw that base class pointers may be used
|
||||
to point to derived class objects. We also saw that when a base class pointer
|
||||
points to an object of a derived class it is the the type of the pointer
|
||||
rather than the type of the object it points to what determines which member
|
||||
functions are visible. So when a tt(Vehicle *vp), points to an tt(Auto) object
|
||||
tt(Auto)'s tt(speed) or tt(brandName) members can't be used.
|
||||
|
||||
Fortunately, a remedy is available. In bf(C++) a tt(Vehicle *vp) may call a
|
||||
function tt(Truck::weight()) when the pointer actually points to a tt(Truck).
|
||||
In the previous chapter two fundamental ways classes may be related to each
|
||||
other were discussed: a class may be emi(implemented-in-terms-of) another
|
||||
class and it can be stated that a derived class emi(is-a) base class. The
|
||||
former relationship is usually implemented using composition, the latter
|
||||
is usually implemented using a special form of inheritance, called
|
||||
emi(polymorphism), the topic of this chapter.
|
||||
|
||||
The terminology for this feature is emi(polymorphism): it is as though the
|
||||
pointer tt(vp) changes its type from a base class pointer to a pointer to the
|
||||
class of the object it actually points to. So, tt(vp) might behave like a
|
||||
tt(Truck *) when pointing to a tt(Truck), and like an tt(Auto *) when pointing
|
||||
to an tt(Auto) etc..+footnote(In one of the StarTrek movies, Capt. Kirk was
|
||||
in trouble, as usual. He met an extremely beautiful lady who, however,
|
||||
later on changed into a hideous troll. Kirk was quite surprised, but the lady
|
||||
told him: ``Didn't you know I am a polymorph?'')
|
||||
An em(is-a) relationship between classes allows us to apply the
|
||||
emi(Liskow Substitution Principle) (emi(LSP)) according to which a derived
|
||||
class object may be passed to and used by code expecting a pointer or
|
||||
reference to a base class object. In the annotation() so far the LSP has been
|
||||
applied many times. Every time an tt(ostringstream, ofstream) or tt(fstream)
|
||||
was passed to functions expecting an tt(ostream) we've been applying this
|
||||
principle. In this chapter we'll discover how to design our own classes
|
||||
accordingly.
|
||||
|
||||
Polymorphism is implemented by a feature called emi(late binding). It's called
|
||||
that way because the decision em(which) function to call (a base class
|
||||
function or a function of a derived class) cannot be made emi(compile-time),
|
||||
LSP is implemented using a technique called emi(polymorphism): although a base
|
||||
class pointer is used it will perform actions defined in the (derived) class
|
||||
of the object it actually points to. So, a tt(Vehicle *vp) might behave like
|
||||
an tt(Auto *) when pointing to an tt(Auto)footnote(In one of the StarTrek
|
||||
movies, Capt. Kirk was in trouble, as usual. He met an extremely beautiful
|
||||
lady who, however, later on changed into a hideous troll. Kirk was quite
|
||||
surprised, but the lady told him: ``Didn't you know I am a polymorph?'').
|
||||
|
||||
Polymorphism is implemented using a feature called emi(late binding). It's
|
||||
called that way because the decision em(which) function to call (a base class
|
||||
function or a function of a derived class) cannot be made em(compile-time),
|
||||
but is postponed until the program is actually executed: only then it is
|
||||
determined which member function will actually be called.
|
||||
|
||||
Note that in bf(C++) late binding is em(not) the default way functions are
|
||||
called. By default emi(static binding) (or emi(early binding)) is used: the
|
||||
class types of objects, object pointers or object refences determine which
|
||||
member functions are called. Late binding is an inherently different (and
|
||||
somewhat slower) procedure since it is decided i(run-time), rather than
|
||||
i(compile-time) what function is called (see section ref(howpolymorphism) for
|
||||
details). As bf(C++) supports em(both) late- and early-binding bf(C++)
|
||||
programmers are offered an option in what kind of binding to use, and so
|
||||
choices can be optimized to the situations at hand. Many other languages
|
||||
offering object oriented facilities (e.g., bf(Java)) only offer late
|
||||
binding. bf(C++) programmers should be keenly aware of this, as expecting
|
||||
early binding and getting late binding might easily produce nasty bugs.
|
||||
In bf(C++) late binding is em(not) the default way functions are called. By
|
||||
default emi(static binding) (or emi(early binding)) is used. With static
|
||||
binding the functions that are called are determined by the compiler, merely
|
||||
using the class types of objects, object pointers or object refences.
|
||||
|
||||
Let's have a look at a simple example (put here even though polymorphism
|
||||
hasn't been covered yet at this point in order to have the example stand
|
||||
clearly) to hone our awareness of the differences between early and late
|
||||
binding. The example merely illustrates. Explanations of em(why) things are as
|
||||
shown are found in subsequent sections of this chapter.
|
||||
Late binding is an inherently different (and slightly slower) process as it is
|
||||
decided i(run-time), rather than i(compile-time) what function will be
|
||||
called. As bf(C++) supports em(both) late- and early-binding bf(C++)
|
||||
programmers are offered an option as to what kind of binding to use. Choices
|
||||
can be optimized to the situations at hand. Many other languages offering
|
||||
object oriented facilities (e.g., bf(Java)) only or by default offer late
|
||||
binding. bf(C++) programmers should be keenly aware of this. Expecting early
|
||||
binding and getting late binding may easily produce nasty bugs.
|
||||
|
||||
The following (using in-class implementations to reduce its size) shows a
|
||||
little program that may be compiled and run:
|
||||
Let's look at a simple example to start appreciating the differences between
|
||||
late and early binding. The example merely illustrates. Explanations of
|
||||
em(why) things are as shown will shortly be provided.
|
||||
|
||||
Consider the following little program:
|
||||
verbinclude(polymorphism/examples/notvirtual.cc)
|
||||
This program could have been constructed from some predecessor, in which
|
||||
maybe just one class was defined. At some point its author decided that it
|
||||
would have been nice to have a separate tt(Base) class, maybe in order to
|
||||
factor out common functionality to be used by various derived classes. Note,
|
||||
for example, how the tt(derived) object calls tt(process()) which is defined
|
||||
in tt(Base): that's common functionality. Unfortunately, an error has crept
|
||||
in. The aim (which is automatically realized in many other object oriented
|
||||
programming languages) was that specialized functionality would be made
|
||||
available in derived classes. So, tt(Derived) re-implements tt(hello()). Alas:
|
||||
when run, the program displays tt(base hello). What went wrong? The answer is:
|
||||
static binding. Due to static binding tt(process()) only knows about
|
||||
tt(Base::hello()), and so that function is called. Polymorphism, which is not
|
||||
The important characteristic of the above program is the tt(Base::process)
|
||||
function, calling tt(hello). As tt(process) is the only member that is defined
|
||||
in the public interface it is the only member that can be called by code not
|
||||
belonging to the two classes. The class tt(Derived), derived from tt(Base)
|
||||
clearly inherits tt(Base)'s interface and so tt(process) is also available in
|
||||
tt(Derived). So the tt(Derived) object in tt(main) is able to call
|
||||
tt(process), but not tt(hello).
|
||||
|
||||
So far, so good. Nothing new, all this was covered in the previous
|
||||
chapter. One may wonder why tt(Derived) was defined at all. It was
|
||||
presumably defined to create an implementation of tt(hello) that's appropriate
|
||||
for tt(Derived) but differing from tt(Base::hello)'s
|
||||
implementation. tt(Derived)'s author's reasoning was as follows: tt(Base)'s
|
||||
implementation of tt(hello) is not appropriate; a tt(Derived) class object can
|
||||
remedy that by providing an appropriate implementation. Furthermore our author
|
||||
reasoned:
|
||||
quote(``since the type of an
|
||||
object determines the interface that is used, tt(process) must call
|
||||
tt(Derived::hello) as tt(hello) is called via tt(process) from a tt(Derived)
|
||||
class object''.)
|
||||
|
||||
Unfortunately our author's reasoning is flawed, due to static binding. When
|
||||
tt(Base::process) was compiled static binding caused the compiler to fixate
|
||||
the tt(hello) call to tt(Base::hello()).
|
||||
|
||||
The author em(intended) to create a tt(Derived) class that tt(is-a) tt(Base)
|
||||
class. That only partially succeeded: tt(Base)'s interface was inherited, but
|
||||
after that tt(Derived) has relinquished all control over what happens. Once
|
||||
we're in tt(process) we're only able to see tt(Base)'s member
|
||||
implementations. Polymorphism offers a way out, allowing us to redefine (in a
|
||||
derived class) members of a base class allowing these redefined members to be
|
||||
used from the base class's interface.
|
||||
|
||||
This is the essence of LSP: public inheritance should not be used to reuse the
|
||||
base class members (in derived classes) but to be reused (by the base class,
|
||||
polymorphically using derived class members reimplementing base class
|
||||
members).
|
||||
|
||||
Take a second to appreciate the implications of the above little program. The
|
||||
tt(hello) and tt(process) members aren't too impressive, but the implications
|
||||
of the example are. The tt(process) member could implement directory travel,
|
||||
tt(hello) could define the action to perform when encountering a
|
||||
file. tt(Base::hello) might simply show the name of a file, but
|
||||
tt(Derived::hello) might delete the file; might only list its name if its
|
||||
younger than a certain age; might list its name if it contains a certain text;
|
||||
etc., etc.. Up to now tt(Derived) would have to implement tt(process)'s
|
||||
actions itself; Up to now code expecting a tt(Base) class reference or pointer
|
||||
could only perform tt(Base)'s actions. Polymorphism allows us to reimplement
|
||||
members of base classes and to use those reimplemented members in code
|
||||
expecting base class references or pointers. Using polymorphism existing code
|
||||
may be reused by derived classes reimplementing the appropriate members of
|
||||
their base classes. It's about time to uncover how this magic can be realized.
|
||||
|
||||
|
||||
Polymorphism, which is not
|
||||
the default in bf(C++), solves the problem and allows the author of the
|
||||
classes to reach its goal. For the curious reader: prefix tt(void hello()) in
|
||||
the tt(Base) class with the keyword tt(virtual) and recompile. Running the
|
||||
|
|
|
@ -1,75 +1,77 @@
|
|||
Until now the base class tt(Vehicle) contained its own, concrete,
|
||||
implementations of the virtual functions tt(weight()) and tt(setWeight()). In
|
||||
bf(C++) it is also possible only to em(mention) virtual member functions in a
|
||||
i(base class), without actually defining them. The functions are concretely
|
||||
implemented in a i(derived class). This approach, in some languages (like
|
||||
bf(C#, Delphi)hi(Java interface) and bf(Java)) known as an emi(interface),
|
||||
defines a emi(protocol), which em(must) be implemented by derived
|
||||
classes. This implies that derived classes must take care of the actual
|
||||
definition: the bf(C++) compiler will not allow the definition of an object of
|
||||
a class in which one or more member functions are left undefined. The base
|
||||
class thus enforces a protocol by declaring a function by its name, return
|
||||
value and arguments. The derived classes must take care of the actual
|
||||
i(implementation). The base class itself defines therefore only a em(model) or
|
||||
em(mold), to be used when other classes are derived. Such base classes are
|
||||
also called hi(class: abstract)
|
||||
emi(abstract classes)
|
||||
or em(abstract base classes). Abstract base classes are the foundation of
|
||||
many em(design patterns) hi(design pattern)
|
||||
(cf. em(Gamma et al.) (1995))hi(Gamma, E.),
|
||||
allowing the programmer to create highly emi(reusable software). Some of these
|
||||
design patterns are covered by the Annotations (e.g, the emi(Template Method)
|
||||
in section ref(FORK)), but for a thorough discussion of design patterns the
|
||||
reader is referred to Gamma em(et al.)'s book.
|
||||
The base class tt(Vehicle) is provided with its own concrete implementations
|
||||
of its virtual members (tt(weight) and tt(setWeight)). However, virtual
|
||||
member functions not necessarily em(have) to be implemented in base classes.
|
||||
|
||||
When the implementations of virtual members are omitted from base classes the
|
||||
class imposes requirements upon derived classes. The derived classes are
|
||||
required to provide the `missing implementations'
|
||||
|
||||
Functions that are only declared in the base class are
|
||||
called emi(pure virtual functions). A function is made pure virtual by
|
||||
prefixing the keyword ti(virtual) to its declaration and by postfixing it
|
||||
with ti(= 0). An example of a pure virtual function occurs in the following
|
||||
listing, where the definition of a class tt(Object) requires the
|
||||
implementation of the i(conversion operator) ti(operator string()):
|
||||
This approach, in some languages (like bf(C#, Delphi)
|
||||
hi(Java interface) and bf(Java)) known as an emi(interface), defines a
|
||||
emi(protocol). Derived classes em(must) obey the protocol by implementing the
|
||||
as yet not implemented members. If a class contains at least one member whose
|
||||
implementation is missing no objects of that class can be defined.
|
||||
|
||||
Such incompletely defined classes are always base classes. They enforce a
|
||||
protocol by merely declaring names, return values and arguments of some of
|
||||
their members. These classes are call hi(class: abstract)
|
||||
emi(abstract classes) or em(abstract base classes). Derived classes become
|
||||
non-abtract classes by implementing the as yet not implemented members.
|
||||
|
||||
Abstract base classes are the foundation of many em(design patterns)
|
||||
hi(design pattern) (cf. em(Gamma et al.) (1995))
|
||||
hi(Gamma, E.), allowing the programmer to create highly
|
||||
emi(reusable software). Some of these design patterns are covered by the
|
||||
annotations() (e.g, the emi(Template Method) in section ref(FORK)), but for a
|
||||
thorough discussion of design patterns the reader is referred to Gamma em(et
|
||||
al.)'s book.
|
||||
|
||||
Members that are merely declared in base classes are called
|
||||
emi(pure virtual functions). A virtual member becomes a pure virtual member
|
||||
by postfixing ti(= 0) to its declaration (i.e., by replacing the semicolon
|
||||
ending its declaration by `tt(= 0;)'). Example:
|
||||
verb(
|
||||
#include <string>
|
||||
|
||||
class Object
|
||||
#include <iosfwd>
|
||||
class Base
|
||||
{
|
||||
public:
|
||||
virtual operator std::string() const = 0;
|
||||
virtual ~Base();
|
||||
virtual std::ostream &insertInto(std::ostream &out) const = 0;
|
||||
};
|
||||
inline std::ostream &operator<<(std::ostream &out, Base const &base)
|
||||
{
|
||||
return base.insertInto(out);
|
||||
}
|
||||
)
|
||||
Now, all classes derived from tt(Object) em(must) implement the
|
||||
tt(operator string()) member function, or their objects cannot be
|
||||
constructed. This is neat: all objects derived from tt(Object) can now always
|
||||
be considered tt(string) objects, so they can, e.g., be inserted into
|
||||
ti(ostream) objects.
|
||||
All classes derived from tt(Object) em(must) implement the tt(insertInto)
|
||||
member function, or their objects cannot be constructed. This is neat: all
|
||||
objects derived from tt(Object) can now always be inserted into ti(ostream)
|
||||
objects.
|
||||
|
||||
Should the i(virtual destructor) of a base class be a pure virtual
|
||||
function? The answer to this question is no: a class such as tt(Vehicle)
|
||||
should not em(require) derived classes to define a
|
||||
destructor. In contrast, tt(Object::operator string()) em(can) be a pure
|
||||
virtual function: in this case the base class defines a protocol which must be
|
||||
adhered to.
|
||||
Could the i(virtual destructor) of a base class ever be a pure virtual
|
||||
function? The answer to this question is no. First of all, there is no need to
|
||||
enforce the availability of destructors in derived classes as destructors are
|
||||
provided by default (unless a destructor is declared with the tt(= delete)
|
||||
attribute using the new C++0x standard). Second, if it is a pure virtual
|
||||
member its implementation does not exist, but derived class destructors will
|
||||
eventually call their base class destructors. How could they call base class
|
||||
destructors if their implementations are lacking? More about this in the next
|
||||
section.
|
||||
|
||||
Note what would happen if we would define the destructor of a base
|
||||
class as a pure virtual destructor: according to the emi(compiler), the
|
||||
derived class object can be constructed: as its destructor is defined, the
|
||||
derived class is not a pure abstract class. However, inside the derived class
|
||||
destructor, the destructor of its base class is implicitly called. This
|
||||
destructor was never defined, and the emi(linker) will loudly complain about
|
||||
an i(undefined reference) to, e.g., tt(Virtual::~Virtual()).
|
||||
|
||||
Often, but not necessarily always, pure virtual member functions are
|
||||
Often, but not necessarily, pure virtual member functions are
|
||||
tt(const) hi(const member functions) member functions. This allows the
|
||||
construction of constant derived class objects. In other situations this might
|
||||
not be necessary (or realistic), and i(non-constant member functions) might be
|
||||
required. The general rule for tt(const) member functions applies also to pure
|
||||
virtual functions: if the member function will alter the object's data
|
||||
members, it cannot be a tt(const) member function. Often abstract base classes
|
||||
have hi(classes: without data members) i(no data members). However, the
|
||||
prototype of the pure virtual member function must be used again in derived
|
||||
not be necessary (or realistic), and
|
||||
i(non-constant member functions) might be required. The general rule for
|
||||
tt(const) member functions also applies to pure virtual functions: if the
|
||||
member function alters the object's data members, it cannot be a tt(const)
|
||||
member function.
|
||||
|
||||
Abstract base classes frequently don't have
|
||||
hi(classes: without data members) data members. However, once a base class
|
||||
declares a pure virtual member it em(must) be declared identically in derived
|
||||
classes. If the implementation of a pure virtual function in a derived class
|
||||
alters the data of the derived class object, than em(that) function cannot be
|
||||
declared as a tt(const) member function. Therefore, the author of an
|
||||
abstract base class should carefully consider whether a pure virtual member
|
||||
function should be a tt(const) member function or not.
|
||||
alters the derived class object's data, then em(that) function cannot be
|
||||
declared as a tt(const) member. Therefore, the author of an abstract base
|
||||
class should carefully consider whether a pure virtual member function should
|
||||
be a tt(const) member function or not.
|
||||
|
|
Loading…
Reference in a new issue