git-svn-id: https://cppannotations.svn.sourceforge.net/svnroot/cppannotations/trunk@329 f6dd340e-d3f9-0310-b409-bdd246841980
This commit is contained in:
Frank B. Brokken 2009-12-19 15:15:23 +00:00
parent d14713f960
commit bced62d86c
7 changed files with 308 additions and 276 deletions

View file

@ -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).

View file

@ -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

View 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

View file

@ -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.

View file

@ -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.

View file

@ -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

View file

@ -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.