File: inheritance.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 (142 lines) | stat: -rw-r--r-- 7,544 bytes parent folder | download | duplicates (4)
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
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
Inheritance should not be applied automatically and thoughtlessly. Often
composition can be used instead, improving on a class's design by reducing
coupling. When inheritance is used em(public) inheritance should not
automatically be used but the type of inheritance that is selected should
match the programmer's intent.

We've seen that polymorphic classes on the one hand offer interface members
defining the functionality that can be requested of base classes and on the
other hand offer virtual members that can be overridden. One of the signs of
good class design is that member functions are designed according to the
principle of `one function, one task'. In the current context: a class member
should either be a member of the class's public or protected interface or it
should be available as a virtual member for reimplementation by derived
classes. Often this boils down to virtual members that are defined in the base
class's em(private) section. Those functions shouldn't be called by code using
the base class, but they exist to be overridden by derived classes using
polymorphism to redefine the base class's behavior.

The underlying principle was mentioned before in the introductory paragraph
of this chapter: according to the emi(Liskov Substitution Principle)
(emi(LSP)) an emi(is-a) relationship between classes (indicating that a
derived class object em(is a) base class object) implies that a derived class
object may be used in code expecting a base class object.

In this case inheritance is used em(not) to let the derived class use the
facilities already implemented by the base class but to reuse the base class
polymorphically by reimplementing the base class's virtual members in the
derived class.

In this section we'll discuss the reasons for using inheritance. Why should
inheritance (not) be used? If it is used what do we try to accomplish by it?

Inheritance often competes with composition. Consider the following two
alternative class designs:
        verb(    class Derived: public Base
    { ... };

    class Composed
    {
        Base d_base;
        ...
    };)

Why and when prefer tt(Derived) over tt(Composed) and vice versa? What
kind of inheritance should be used when designing the class tt(Derived)?
    itemization(
    it() Since tt(Composed) and tt(Derived) are offered as alternatives we are
        looking at the design of a class (tt(Derived) or tt(Composed)) that
        emi(is-implemented-in-terms-of) another class.
    it() Since tt(Composed) does itself not make tt(Base)'s interface
        available, tt(Derived) shouldn't do so either. The underlying
        principle is that emi(private inheritance) should be used when
        deriving a classs tt(Derived) from tt(Base) where tt(Derived)
        is-implemented-in-terms-of tt(Base).
    it() Should we use inheritance or composition? Here are some arguments:
        itemization(
        it() In general terms composition results in looser coupling and
            should therefore be preferred over inheritance.
        it() Composition allows us to define classes having multiple members
            of the same type (think about a class having multiple
            tt(std::string) members) which can not be realized using
            inheritance.
        it() Composition allows us to separate the class's interface from its
            implementation. This allows us to modify the class's data
            organization without the need to recompile code using our
            class. This is also known as the emi(bridge design pattern) or the
            emi(compiler firewall) or emi(pimpl) (pointer to the
            implementation) idiom.
        it() If tt(Base) offers members in its em(protected) interface that
            must be used when implementing tt(Derived) inheritance must also
            be used. Again: since we're implementing-in-terms-of the
            inheritance type should be tt(private).
        it() Protected inheritance may be considered when the derived class
            (tt(D)) itself is intended as a base class that should only make
            the members of its own base class (tt(B)) available to classes
            that are derived from it (i.e., tt(D)).
        )
    )

    Private inheritance should also be used when a derived class is-a certain
type of base class, but in order to initialize that base class an object of
another class type must be available. Example: a new tt(istream) class-type
(say: a stream tt(IRandStream) from which random numbers can be extracted) is
derived from tt(std::istream).  Although an tt(istream) can be constructed
empty (receiving its tt(streambuf) later using its tt(rdbuf) member), it is
clearly preferable to initialize the tt(istream) base class right away.

Assuming that a tt(Randbuffer: public std::streambuf) has been created for
generating random numbers then tt(IRandStream) can be derived from
tt(Randbuffer) and tt(std::istream). That way the tt(istream) base class can
be initialized using the tt(Randbuffer) base class.

As a tt(RandStream) is definitely not a tt(Randbuffer) em(public) inheritance
is em(not) appropriate. In this case tt(IRandStream)
is-implemented-in-terms-of a tt(Randbuffer) and so em(private) inheritance
should be used.

tt(IRandStream)'s class interface should therefore start like this:
        verb(    class IRandStream: private Randbuffer, public std::istream
    {
        public:
            IRandStream(int lowest, int highest)    // defines the range
            :
                Randbuffer(lowest, highest),
                std::istream(this)                  // passes &Randbuffer
            {}
        ...
    };)

Public inheritance should be reserved for classes for which the LSP holds
true. In those cases the derived classes can always be used instead of the
base class from which they derive by code merely using base class references,
pointers or members (I.e., conceptually the derived class emi(is-a) base
class). This most often applies to classes derived from base classes offering
virtual members. To separate the user interface from the redefinable interface
the base class's public interface should em(not) contain virtual members
(except for the virtual destructor) and the virtual members should all be in
the base class's private section. Such virtual members can still be overridden
by derived classes (this should not come as a surprise, considering how
polymorphism is implemented) and this design offers the base class full
control over the context in which the redefined members are used. Often the
public interface merely calls a virtual member, but those members can always
be redefined to perform additional duties.

    The prototypical form of a hi(base class: prototype) base class therefore
looks like this:
        verb(    class Base
    {
        public:
            virtual ~Base();
            void process();             // calls virtual members (e.g.,
                                        // v_process)
        private:
            virtual void v_process();   // overridden by derived classes
    };)

Alternatively a base class may offer a non-virtual destructor, which
should then be protected. It shouldn't be public to prevent deleting objects
through their base class pointers (in which case virtual destructors should be
used). It should be protected to allow derived class destructors to call their
base class destructors. Such base classes should, for the same reasons, have
non-public constructors and overloaded assignment operators.