File: intro.yo

package info (click to toggle)
c%2B%2B-annotations 13.02.02-1
  • links: PTS, VCS
  • area: main
  • in suites: forky, sid
  • size: 13,576 kB
  • sloc: cpp: 25,297; makefile: 1,523; ansic: 165; sh: 126; perl: 90; fortran: 27
file content (116 lines) | stat: -rw-r--r-- 7,013 bytes parent folder | download | duplicates (2)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
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 the pointer's type, rather than the
object's type, determines which member functions are visible. So when a
tt(Vehicle *vp), points to a tt(Car) object tt(Car)'s tt(speed) or
tt(brandName) members can't be used.

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.

An em(is-a) relationship between classes allows us to apply the
 emi(Liskov 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 annotations() 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.

LSP is implemented using a technique called emi(polymorphism): although a base
class pointer is used it performs actions defined in the (derived) class
of the object it actually points to. So, a tt(Vehicle *vp) might behave like
a tt(Car *) when pointing to a tt(Car)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 at em(compile-time),
but is postponed until the program is actually executed: only then it is
determined which member function will actually be called.

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

Late binding is an inherently different (and slightly slower) process as it is
decided at i(run-time), rather than at i(compile-time) what function is going
to 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.

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 are shortly provided.

Consider the  following little program:
        verbinclude(-a examples/notvirtual.cc)
    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 bind
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 modified program produces the intended and expected
tt(derived hello). Why this happens is explained next.