Author : Ian Joyner
Page : << Previous 4 Next >>
environments
where systems are assembled out of preexisting components. Unless the
programmer explicitly overrides the original name a syntax error should
report that the name is a duplicate declaration. C++, however, adopted
the original approach of Simula. This approach has been improved upon,
and other languages have adopted better, more explicit approaches, that
avoid the error of mistaken redefinition.
Eiffel and Object Pascal cater for this situation as the descendant
class programmer is required to specify that redefinition is intended.
This has the extra benefit that a later reader or maintainer of the
class can easily identify the routines that have been redefined, and
that this definition is related to a definition in an ancestor class
without having to refer to ancestor class definitions. Thus option 2 is
exactly where it should be, in descendant classes.
Option 3
The pure virtual function caters for the third option. The routine
is undefined, the class is abstract and cannot be directly instantiated.
A descendant class must define the routine if it is to be instantiated.
Any descendants that do not define the routine are also abstract
classes. This concept is correct, but see the section on pure virtual
functions for a criticism of the syntax.
Virtual is a difficult notion to grasp. The related concepts of
polymorphism and dynamic binding, redefinition, and overloading are
easier to grasp, being oriented towards the problem domain. Virtual
routines are an implementation mechanism for polymorphism. Polymorphism
is the 'what', and virtual is the 'how'. Smalltalk and Objective-C use
a different mechanism to implement polymorphism. Virtual is an example
of where C++ obscures the concepts of OOP. The programmer has to come
to terms with low level concepts, rather than the higher level
object-oriented concepts. Interesting as underlying mechanisms might be
for the theoretician or compiler implementer, the practitioner should
not be required to understand or use them to make sense of the higher
level concepts. Having to use them in practice is tedious and
error-prone, and can prevent the adaptation of software to further
advances in the underlying technology and execution mechanisms (see
concurrency).
3.2. Pure Virtual Functions
As mentioned above, pure virtual functions provide a means of leaving
a function undefined and abstract. A class that has such an abstract
function cannot be directly instantiated. A non-abstract descendant
class must define the function. The C++ pure virtual syntax is:
virtual void fn () = 0;
This leaves the reader to guess its meaning, even those well
versed in object-oriented concepts. A better choice would have been
a keyword such as 'abstract'. Direct expression of concepts enhances
communication, and the ease with which a language can be learnt.
When learning a language it is often important to use the index of
a text book. A keyword like 'abstract' would be easily found in an
index. But what do you look for in the case of '= 0'? You might not
even realise it is significant. It should have syntactic significance
as abstract functions are a very important concept in object-
oriented design. The C++ decision is in keeping with the C philosophy
of avoiding keywords. This is often at the expense of clarity.
A keyword would implement this concept more clearly. For example -
pure virtual void fn ();
or
abstract void fn ();
The mathematical notation used in C++ suggests that values other than
zero could be used. What if the function is equated to 13? -
virtual void fn () = 13;
A function is either pure, or it is not. This to any analyst suggests
a boolean state, which a single keyword conveys. A simple
suggestion to fix this is to define '= 0' as abstract:
#define abstract = 0
then
virtual void fn () abstract;
'Pure virtual' is also an abuse of natural language. It is a
combination of words that are somewhat opposite in meaning. Pure means
something that really is what it appears to be. For example pure gold.
Virtual means something that appears to be what it actually is not. For
example virtual memory. Perhaps virtual gold could be fools gold. As
has been said before, virtual is a difficult concept to grasp. When it
is combined with a word such as 'pure', the meaning becomes even more
obscure. Modern language designers should be very careful in the
vocabulary they choose.
3.3. The Nature of Inheritance
Inheritance is a close relationship. It provides a fundamental way
to assemble software components. Objects that are instances of a class
are also instances of all ancestors of that class. For effective
object-oriented design the consistency of this relationship should be
preserved. Each redefinition in a subclass should be checked for
consistency with the original definition in an ancestor class. A
subclass should preserve the requirements of an ancestor class.
Requirements that cannot be preserved indicate a design error and
perhaps inheritance is not appropriate. Consistency due to inheritance
is fundamental to object-oriented design. C++'s implementation of
non-virtual overloading, and overloading by signature (see below) means
that the compiler cannot check for this consistency. C++ does not
realise this aspect of object-oriented design. This contributes to a
wide and costly gap between analysis and design, and implementation.
Inheritance has been classified as 'syntactic' inheritance and
'semantic' inheritance. Saake et al describe these as follows :
"Syntactic inheritance denotes inheritance of structure or method
definitions and is therefore related to the reuse of code (and to
overriding of code for inherited methods). Semantic inheritance denotes
inheritance of object semantics, ie of objects themselves. This kind of
inheritance is known from semantic data models, where it is used to
model one object that appears in several roles in an application."
[SJE91]. Saake et al concentrate on the semantic form of inheritance.
Behavioural or semantic inheritance expresses the role of an object
within a system.
Wegner, however, believes code inheritance to be of more practical
value. He classifies the difference between syntactic and semantic
inheritance as code and behaviour hierarchies [Weg90] (p43). He
suggests these are rarely compatible with each other and are often
negatively correlated. Wegner also poses the question of "How should
modification of inherited attributes be constrained?" Code inheritance
provides a basis for modularisation. Behavioural inheritance provides
modelling by the 'is-a' relationship. Both are useful in their place.
Both require consistency checks that combinations due to inheritance
actually make sense.
It seems that inheritance is most powerful in the most restrictive
form of a semantics preserving relationship. A subclass should
not break the assumptions of an ancestor class.
Software components are like jig-saw pieces. When assembling a
jig-saw the shape of the pieces must fit, but more importantly, the
resulting picture must make sense. Assembling software components is
more difficult. A jig-saw is reassembling a picture that was complete
before. Assembling software components is building a system that has
never existed before.
Inheritance in C++ is like a jig-saw where the pieces fit together,
but the compiler has no way of checking that the resultant picture makes
sense. In other words C++ has provided the syntax for classes and
inheritance but not the semantics. Certainly, not very many reusable
C++ libraries are available, which suggests that C++ might not support
reusability as well as possible. C++ fails to provide this fundamental
goal of object-oriented design and programming.
3.4. Function Overloading
C++ allows functions to be overloaded if the arguments in the
signature are of different types. Such overloading can be useful as
these examples show:
max (int, int);
max (real, real);
This will ensure that the best max routine for the types int and real
will be invoked. Object-oriented programming, however, provides a
variant on this. Since the object is passed to the routine as a hidden
parameter ('this' in C++), an equivalent but more restricted form is
already implicitly included in object-oriented concepts. A simple
example such as the above would be expressed as:
int i, j;
real r, s;
i.max (j);
r.max (s);
but i.max (r) and r.max (j) result in compilation errors because the
types of the arguments do not agree. (By operator overloading of
course, these can be better expressed, i max j and r max s, but min and
max are peculiar functions that might want to accept two or more
parameters of the same type.)
The above shows that in most cases, the object-oriented paradigm can
consistently express function overloading, without the need for the
function overloading of C++. C++, however, does make the notion more
general. The advantage is that more than one parameter can overload a
function, not
Page : << Previous 4 Next >>