Author : Ian Joyner
Page : << Previous 5 Next >>
just the implicit current object parameter.
The disadvantage is that C++ introduces some inconsistencies that the
compiler cannot detect. If the programmer intends to redefine a virtual
routine, but makes a mistake in the declaration of the function
signature, the compiler will erroneously assume an overloaded function.
Any calls to the function using one or other of the signatures will also
fail to detect the inconsistency.
When calling the routine, if the programmer makes a mistake in
supplying the actual parameters, a C++ compiler cannot be specific about
the error. It can only report that no function with a matching
signature could be found. Programmers make this sort of mistake for
subtle reasons, and it can be time consuming to pinpoint the parameter
at fault. Secondly, the incorrect parameter might accidentally match,
one of the other routines. In that case this error will be propagated
into the production code, and could remain undetected a long time.
If it is felt that C++'s scheme of having parameters of different
types is useful, it should be realised that object-oriented programming
provides this in a more restricted and disciplined form. This is done
by specifying that the parameter needs to conform to a base class. Any
parameter passed to the routine can only be a type of the base class, or
a subclass of the base class. For example:
A.f (B someB) {...};
class B ...;
class D : public B ...
A a;
D d;
a.f (d);
The entity 'd' must conform to the class 'B', and the compiler checks
this.
The alternative to function overloading by signature, is to require
functions with different signatures to have different names. Names
should be the basis of distinction of entities. This is known to work
and solves the above problems. The compiler can cross check that the
parameters supplied are correct for the given routine name. This also
results in better self-documented software. It is often difficult to
choose appropriate names for entities, but it is well worth the effort.
3.5. Virtual Classes
If class D multiply inherits class A via classes B and C, then if D
wants to inherit only a single copy of A, the inheritance of A must be
specified as virtual in both B and C. This raises two questions.
Firstly, what happens if A is declared virtual in only one of B or C?
Secondly, what if another class E wants to inherit multiple copies of A
via B and C? In C++, the virtual class decision must be made early,
reducing the flexibility that might be required in the assembly of
derived classes. In a shared software environment different vendors
might supply classes B and C. It should be left to the implementer of
class D or E, exactly how to resolve this problem. And this is the
simplest case. What if A is inherited via more than two paths, with
more than two levels of inheritance? Such flexibility is key to reusable
software. You cannot envisage when designing a base class all the
possible uses in derived classes, and attempting to do so considerably
complicates design.
3.6. Name overloading
Naming is fundamentally important in producing self-documenting
software. Naming helps realise maintainable and reusable software
components. Names are fundamental in freeing programmers from low level
manipulation of addresses. Naming is the basis for differentiating
between different entities in a software module. Name overloading
allows the same name to refer to two or more different entities. The
problem is whether the resultant ambiguity is useful, and how to resolve
it, as ambiguity weakens the power of names to distinguish entities.
Name overloading is useful for two purposes. Firstly it allows
programmers to work on two or more modules without concern about name
clashes. The ambiguity can be tolerated as within the context of each
module, the name unambiguously refers to a unique entity. Secondly,
name overloading provides polymorphism, where the same name applied to
different types refers to different implementations for those types.
Polymorphism allows one word to describe 'what' is to be computed.
Different classes might require different specifications of 'how', a
computation is done. For example 'draw' is an operation that is
applicable to all different shapes, even though circles and squares, etc
are 'drawn' differently.
These two uses of name overloading provide a powerful concept. But
use of the same name in the same context must be resolved. Errors can
result from ambiguity. In this case the programmer needs to
differentiate between entities in ways other than name alone. A common
way to do this is to introduce extra distinguishing names. For example
in a group of people, where two or more share the same first name, they
can be distinguished by their surname. Similarly a unique first name
will distinguish the members of a family with a common surname.
This is analogous to classes, where each class in a system is given a
unique name. Each member within a class is also given a unique name.
Where two objects with members of the same name are used within the same
context, the object name can qualify the members. For example a.mem and
b.mem.
[Reade 89] points out the difference between overloading and
polymorphism. Overloading means the use of the same name in the same
context for different entities with completely different definitions and
types. Polymorphism though has one definition, and all types are
subtypes of a principle type. C. Strachey referred to polymorphism as
parametric polymorphism and overloading as ad hoc polymorphism.
Block structured languages provide overloading by scoping. Scoping
allows the same name to be used in different contexts without clash or
confusion. Nested blocks provide a subtle problem. Names in an outer
block are in scope in inner blocks. Many languages, however, allow a
name to be overloaded in an inner block. This does more than overload
the name, it hides it. The use of a name in the inner block does not
indicate any relationship with the same name in the outer block.
Textually nested blocks 'inherit' named entities from outer blocks.
Inheritance accomplishes this in object-oriented languages. Inheritance
eliminates the need to textually nest entities, and also accomplishes
loose coupling. Nesting makes entities tightly coupled.
Contrary to most high level languages, a name should not be
overloaded while it is in scope. This inconveniently hides the outer
declaration, and the programmer cannot access the outer entity. It is
also error prone. The following example illustrates this:
{
int i;
{
int i; // hide the outer i.
i = 13; // assign to the inner i.
// Can't get to the outer i here.
// It is in scope, but hidden.
}
}
Now delete the inner declaration:
{
int i;
{
i = 13; // Syntactically valid, but not the intention.
}
}
The inner overloaded declaration is removed, and references to that
name do not result in syntax errors due to the same name being in the
outer environment. The inner instruction now mistakenly changes the
value of the outer entity. A compiler cannot detect this situation
unless the language definition forbids nested redeclarations. E.W.
Dijkstra uses similar reasoning in 'An essay on the Notion: "The Scope
of Variables"' in "A Discipline of Programming", [Dijkstra 76].
The above example demonstrates how nesting results in unmaintainable
programs. This is because the inner block is tightly coupled to the
outer block, and each is sensitive to changes in the other. The
advantage of keeping components decoupled and separate is that a
programmer can confidently make modifications to one component without
affecting other components. Testing can be limited to the changed
component, rather than a combination of components, which quickly leads
to an exponentiation in the number of tests required.
C++ has an analogous form of hiding. A non-virtual function in a
derived class hides a function in an ancestor class. This hiding is
explained in section 13.1 of the C++ ARM. This is a discrepancy with
declaring multiple functions with the same name in the same class with
different signatures. A function in the derived class will hide the
functions of the ancestor class, rather than add its signature to the
list of possible functions which can be called. This is confusing and
error prone. Learning all these ins and outs of the language is
extremely burdensome to the programmer. Often they will only be learnt
after falling into a trap.
3.7. Polymorphism and Inheritance
Inheritance provides a form of name overloading similar to
overloading in subblocks. The scope of a name is the class in which it
occurs. If a name occurs
Page : << Previous 5 Next >>