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 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192
|
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 *) activates 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 determined at
i(compile-time). In bf(C++) em(late)
hi(late binding) 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). 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 base class, it remains virtual in
all derived classes. The keyword tt(virtual) should not be mentioned for
members in derived classes which are declared virtual in base classes. In
derived classes those members should be provided with the ti(override)
indicator, allowing the compiler to verify that you're indeed referring to an
existing virtual member function.
In the vehicle classification system (see section ref(VehicleSystem)), let's
concentrate on the members tt(mass) and tt(setMass). These members define the
emi(user interface) of the class tt(Vehicle). What we would like to accomplish
is that this user interface can be used for tt(Vehicle) and for any class
inheriting from tt(Vehicle), since objects of those classes are themselves
also tt(Vehicles).
If we can define the user interface of our base class (e.g., tt(Vehicle)) such
that it remains usable irrespective of the classes we derive from tt(Vehicle)
our software achieves an enormous reusability: we design our software around
tt(Vehicle's) user interface, and our software will also properly function for
derived classes. Using plain inheritance doesn't accomplish this. If we define
verb( std::ostream &operator<<(std::ostream &out, Vehicle const &vehicle)
{
return out << "Vehicle's mass is " << vehicle.mass() << " kg.";
})
and tt(Vehicle's) member tt(mass) returns 0, but tt(Car's) member
tt(mass) returns 1000, then twice a mass of 0 is reported when the following
program is executed:
verb( int main()
{
Vehicle vehicle;
Car vw{ 1000, 160, "Golf" };
cout << vehicle << '\n' << vw << '\n';
})
We've defined an overloaded insertion operator, but since it only knows
about tt(Vehicle's) user interface, `tt(cout << vw)' will use tt(vw's
Vehicle's) user interface as well, thus displaying a mass of 0.
Reusability is enhanced if we add a em(redefinable interface) to the base
class's interface. A redefinable interface allows derived classes to fill in
their own implementation, without affecting the user interface. At the same
time the user interface will behave according to the derived class's wishes,
and not just to the base class's default implementation.
Members of the reusable interface should be declared in the class's
private sections: conceptually they merely belong to their own classes
(cf. section ref(INHERITWHY)). In the base class these members should be
declared tt(virtual). These members can be redefined (overridden) by derived
classes, and should there be provided with tt(override) indicators.
We keep our user interface (tt(mass)), and add the redefinable member
tt(vmass) to tt(Vehicle's) interface:
verb( class Vehicle
{
public:
size_t mass() const;
size_t si_mass() const; // see below
private:
virtual size_t vmass() const;
};)
Separating the user interface from the redefinable interface is a sensible
thing to do. It allows us to fine-tune the user interface (only one point of
maintenance), while at the same time allowing us to standardize the expected
behavior of the members of the redefinable interface. E.g., in many countries
the International system of units is used, using the kilogram as the unit for
mass. Some countries use other units (like the em(lbs): 1 kg being
approx. 2.2046 lbs). By separating the user interface from the redefinable
interface we can use one standard for the redefinable interface, and keep the
flexibility of transforming the information em(ad-lib) in the user
interface.
Just to maintain a clean separation of user- and redefinable interface we
might consider adding another accessor to tt(Vehicle), providing the
tt(si_mass), simply implemented like this:
verb( size_t Vehicle::si_mass() const
{
return vmass();
})
If tt(Vehicle) supports a member tt(d_massFactor) then its tt(mass) member can
be implemented like this:
verb( size_t Vehicle::mass()
{
return d_massFactor * si_mass();
})
tt(Vehicle) itself could define tt(vmass) so that it returns a token
value. E.g.,
verb( size_t Vehicle::vmass()
{
return 0;
})
Now let's have a look at the class tt(Car). It is derived from
tt(Vehicle), and it inherits tt(Vehicle's) user interface. It also has a data
member tt(size_t d_mass), and it implements its own reusable interface:
verb( class Car: public Vehicle
{
...
private:
size_t vmass() override;
})
If tt(Car) constructors require us to specify the car's mass (stored
in tt(d_mass)), then tt(Car) simply implements its tt(vmass) member like
this:
verb( size_t Car::vmass() const
{
return d_mass;
})
The class tt(Truck), inheriting from tt(Car) needs two mass values: the
tractor's mass and the trailer's mass. The tractor's mass is passed to its
tt(Car) base class, the trailor's mass is passed to its tt(Vehicle d_trailor)
data member. tt(Truck), too, overrides tt(vmass), this time returning the sum
of its tractor and trailor masses:
verb( size_t Truck::vmass() const
{
return Car::si_mass() + d_trailer.si_mass();
})
Once a class member has been declared tt(virtual) it becomes
a virtual member in all derived classes, whether or not these members are
provided with the tt(override) indicator. But tt(override) em(should) be used,
as it allows to compiler to catch typos when writing down the derived class
interface.
A member function may be declared tt(virtual) em(anywhere) in a
i(class hierarchy), but this probably defeats the underlying polymorphic
class design, as the original base class is no longer capable of completely
covering the redefinable interfaces of derived classes. If, e.g, tt(mass) is
declared virtual in tt(Car), but not in tt(Vehicle), then the specific
characteristics of virtual member functions would only be available for
tt(Car) objects and for objects of classes derived from tt(Car). For a
tt(Vehicle) pointer or reference static binding would remain to be used.
The effect of late binding (polymorphism) is illustrated below:
verb( void showInfo(Vehicle &vehicle)
{
cout << "Info: " << vehicle << '\n';
}
int main()
{
Car car(1200); // car with mass 1200
Truck truck(6000, 115, // truck with cabin mass 6000,
"Scania", 15000); // speed 115, make Scania,
// trailer mass 15000
showInfo(car); // see (1) below
showInfo(truck); // see (2) below
Vehicle *vp = &truck;
cout << vp->speed() << '\n';// see (3) below
})
Now that tt(mass) is defined tt(virtual), late binding is used:
itemization(
it() at (1), tt(Car)'s mass is displayed;
it() at (2) tt(Truck)'s mass is displayed;
it() at (3) a syntax error is generated. The member
tt(speed) is not a member of tt(Vehicle), and hence not callable via
a tt(Vehicle*).
)
The example illustrates that when a pointer to a class is used em(only the
members of that class can be called). 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.
Through virtual members derived classes may redefine the behavior performed by
functions called from base class members or from pointers or references to
base class objects. This redefinition of base class members by derived classes
is called hi(member: overriding)emi(overriding members).
|