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
|
With static polymorphism a class template takes the role of a base class in
dynamic polymorphism. This class template declares several members, which are
comparable to members of a polymorphic base class: they are either support
members or they call members that are overridden in derived classes.
In the context of dynamic polymorphism these overridable members are the base
class's virtual members. In the context of static polymorphism there are no
virtual members. Instead, the statically polymorphic base class (referred to
as the `base class' below) declares a em(template type parameter) (referred to
as the `derived class type' below). Next, the base class's interfacing members
call members of the derived class type.
Here is a simple example: a class template acting as a base class. Its public
interface consists of one member. But different from dynamic polymorphism
there's no reference in the class's interface to any member showing
polymorphic behavior (i.e, no `virtual' members are declared):
verb( template <class Derived>
struct Base
{
void interface();
})
Let's have a closer look at the member `tt(interface)'. This member is called
by functions receiving a reference or pointer to the base class, but it may
call members that must be available in the derived class type at the point
where tt(interface) is called. Before we can call members of the derived class
type an object of the derived class type must be available. This object is
obtained through inheritance. The derived class type is going to be derived
from the base class. Thus tt(Base's this) pointer is also tt(Derived's this)
pointer.
Forget about polymorphism for a second: when we have a tt(class Derived:
public Base) then (because of inheritance) a tt(static_cast<Derived *>) can be
used to cast a tt(Base *) to a tt(Derived) object. A tt(dynamic_cast) of
course doesn't apply, as we don't use dynamic polymorphism. But a
tt(static_cast) is appropriate since our tt(Base *) em(does) in fact point to
a tt(Derived) class object.
So, to call a tt(Derived) class member from inside tt(interface) we
can use the following implementation (remember that tt(Base) is a base class
of tt(Derived)):
verb( template<class Derived>
void Base<Derived>::interface()
{
static_cast<Derived *>(this)->polymorphic();
})
It's remarkable that, when the compiler is given this implementation it
cannot determine whether tt(Derived) is em(really) derived from
tt(Base). Neither can it determine whether the class tt(Derived) indeed offers
a member tt(polymorphic). The compiler simply em(assumes) this to be true. If
so, then the provided implementation is syntactically correct. One of the
key characteristics of using templates is that the implementation's viability
is eventually determined at the function's point of instantiation (cf. section
ref(TEMPFUNINST)). At that point the compiler will verify that, e.g., the
function tt(polymorphic) really is available.
Thus, in order to use the above scheme we must ensure that
itemization(
it() derived class type is actually derived from the base class and
it() that the derived class type defines a member
`tt(polymorphic)'.
)
The first requirement is satisfied by using the
emi(curiously recurring template pattern):
verb( class First: public Base<First>)
In this curious pattern the class tt(First) is derived from tt(Base),
which itself is instantiated for tt(First). This is acceptable, as the
compiler already has determined that the type tt(First) exists. At this point
that is all it needs.
The second requirement is simply satisfied by defining the member
tt(polymorphic). In chapter ref(POLYMORPHISM) we saw that virtual and
overriding members belong to the class's private interface. We can apply the
same philosophy here, by placing tt(polymorphic) in tt(First's) private
interface, allowing it to be accessed from the base class by declaring
verb( friend void Base<First>::interface();)
tt(First's) complete class interface can now be designed, followed by
tt(polymorphic's) implementation:
verb( class First: public Base<First>
{
friend void Base<First>::interface();
private:
void polymorphic();
};
void First::polymorphic()
{
std::cout << "polymorphic from First\n";
})
Note that the class tt(First) itself is not a class template: its members
can be separately compiled and stored in, e.g., a library. Also, as is the
case with dynamic polymorphism, the member tt(polymorphic) has full access to
all of tt(First)'s data members and member functions.
Multiple classes can now be designed like tt(First), each offering their
own implementation of tt(polymorphic). E.g., the member
tt(Second::polymorphic) of the class tt(Second), designed like tt(First),
could be implemented like this:
verb( void Second::polymorphic()
{
std::cout << "polymorphic from Second\n";
})
The polymorphic nature of tt(Base) becomes apparent once a function
template is defined in which tt(Base::interface) is called. Again, the
compiler simply assumes a member tt(interface) exists when it reads the
definition of the following function template:
verb( template <class Class>
void fun(Class &object)
{
object.interface();
})
Only where this function is actually called will the compiler verify the
viability of the generated code. In the following tt(main) function a
tt(First) object is passed to tt(fun): tt(First) declares tt(interface)
through its base class, and tt(First::polymorphic) is called by
tt(interface). The compiler will at this point (i.e., where tt(fun) is called)
check whether tt(first) indeed has a member tt(polymorphic). Next a tt(Second)
object is passed to tt(fun), and here again the compiler checks whether
tt(Second) has a member tt(Second::polymorphic):
verb( int main()
{
First first;
fun(first);
Second second;
fun(second);
})
There are also downsides to
hi(static polymorphism: downsides)
using static polymorphism:
itemization(
it() First, the sentence `a tt(Second) object is passed to tt(fun)'
formally isn't correct, since tt(fun) is a function template the functions
tt(fun) called as tt(fun(first)) and tt(fun(second)) are
em(different) functions, not just calls of one function with different
arguments. With static polymorphism every instantiation using its own template
parameters results in completely new code which is generated when the template
(e.g., tt(fun)) is instantiated. This is something to consider when creating
statically polymorphic base classes. If the base class defines data members
and member functions, and if these additional members are used by derived
class types, then each member has its own instantiation for each derived class
type. This also results in i(code bloat), albeit of a different kind than
observed with dynamic polymorphism. This kind of code bloat can often be
somewhat reduced by deriving the base class from its own (ordinary,
non-template) base class, encapsulating all elements of the statically
polymorphic base class that do not depend on its template type parameters.
it() Second, if different types of statically polymorphic objects are
dynamically created (using the tt(new) operator) then the types of the
returned pointers are all different. In addition, the types of the pointers to
their statically polymorphic base classes differ from each other. These
latter pointers are different because they are pointers to tt(Base<Derived>),
representing different types for different tt(Derived) types. Consequently,
and different from dynamic polymorphism, these pointers cannot be collected
in, e.g., a vector of shared pointers to base class pointers. There simply
isn't one base class pointer type. Thus, because of the different base class
types, there's no direct statically polymorphic equivalent to virtual
destructors.
it() Third, as illustrated in the next section, designing static
polymorphic classes using multiple levels of inheritance is not a trivial
task.
)
Summarizing, static polymorphism is best used in situations where a small
number of different derived class types are used, where a fixed number of
derived class objects are used, and where the statically polymorphic base
classes themselves are lean (possibly encapsulating some of their code in
ordinary base classes of their own).
|