From: ian@syacus.acus.oz.au (Ian Joyner)
Subject: C++ Critique (3 of 4) (long)
Summary: Part 3 of C++ Critique
Organization: ACUS Australian Centre for Unisys Software, Sydney
Date: Wed, 8 Apr 1992 11:54:08 GMT

(C++ Critique Part 3)

Calls on references to deallocated objects (Dangling Pointers)
--------------------------------------------------------------

The compromise that C++ does not have garbage-collection means that it is
possible that a routine in a deallocated object may be called.  For example if
an object is deallocated in one place in the program, but another part of the
program still has a reference to this object, then the potential to invalidly
invoke a routine exists.  There is no way to detect that the object has been
deallocated, as the reference will not be NULL, but a 'dangling reference'. 
This contributes to the fragility of C++ programs. 

The second problem is that objects which become useless may miss being
deallocated.  This leads to memory leaks, which means memory gradually becomes
full of dead objects, leading to eventual system failure.  Such memory leaks are
difficult to detect and find.  Such leaks are often only detected after long and
thorough testing of the system. 

Both problems are solved by garbage-collection.  Garbage-collection has
undeservedly earned a bad reputation due to some early implementations
exhibiting some performance problems, instead of working transparently in the
background, as they can and should.  As C++ does not include garbage-collection,
the problems are often over-emphasised as a justification for C++ not including
it. 

It may be argued that the lack of garbage-collection in C++ is not an
engineering compromise, as its inclusion would be an engineering impossibility,
as there are many ways that a programmer can undermine the structures that are
required to implement correctly working garbage-collection.  While
garbage-collection may not actually be an engineering impossibility in C++
(EC++), it is difficult, and programmers would have to settle for a more
restricted way of doing things.  This may be a good thing, but then the
compromise to remain compatible with the C language becomes difficult, if
compiler detection of practices inconsistent with the correct operation of
garbage-collection is required. 


Type-safe linkage
-----------------

As explained in the C++ ARM, type-safe linkage is not 100% type safe.  If it is
not 100% type-safe, then it is not safe.  It is always the subtle errors that
provide the most problems, not the simple or obvious ones.  Such subtle errors
get into the production system, undetected, until a maybe critical moment.  The
seriousness of this situation cannot be underestimated.  As many forms of
transport, such as planes, and space programs depend on software to provide
safety in their operation, lives can depend on software.  Not only this, but
often the financial survival of organisations can depend upon their software. 
Therefore the acceptance of such unsafe situations is at best irresponsible. 

The C++ ARM summarises the situation as follows - "Handling all inconsistencies
- thus making a C++ implementation 100% type-safe - would require either linker
support or a mechanism (an environment) allowing the compiler access to
information from separate compilations."

So why does the C++ compiler (at least AT&T's) not provide for accessing
information from separate compilations, and why is there not a specialised
linker for C++, that actually provides 100% type safety? There is no reason why
C++ should not be implemented in this manner.  Building systems out of
preexisting elements is the common Unix style of software production.  This
implements reusability of a form, but not in the truly flexible manner of
object-oriented reusability. 

This is why in the future, Unix may be replaced by completely object-oriented
operating systems, which are indeed 'open', to be able to be tailored to best
suit the purpose at hand.  By the use of pipes and flags, Unix software elements
can be reused to provide some sort of functionality that approximates what is
desired.This approach is valid and works with efficacy in some instances, like
small in-house applications, or perhaps for research prototyping, but is not
acceptable for widespread and expensive software, or safety critical
applications.  Since C++ is becoming widely used, this sort of implementation
should not be regarded as acceptable.  For C++ implementations that do not have
their own specialised linker, optimisations such as the replacement of virtual
calls with procedure calls may not be possible. 


C++ and the software lifecycle
------------------------------

Much work has been done on defining the software lifecycle.  It is at least
generally accepted that the activities in the lifecycle are analysis of
requirements, design, implementation, testing and error correction, extension. 
Unfortunately, the result of identifying these activities has resulted in a
school of thought that the boundaries between these activities are fixed, and
that they should be systematically separate, one being performed and completed
before the next is commenced.  It is often argued that if they are not cleanly
separated, then you are not practicing disciplined system development.  This
view is quite incorrect.  Someone who sits down and starts writing program text
straight away is actually doing all the steps in parallel.  It may not be the
best way do do things in many circumstances, may or may not suit the style and
thinking of different people, but there are scenarios where this works, and can
be the methodology of choice of disciplined thinkers.  More likely though, the
phases will interact and overlap.  Facts found out only as late as the
implementation stage may be fed back into the analysis and design stages. 

It is now becoming accepted that the software lifecycle should be an integrated
process, that each phase will lead to the next naturally, without unnecessary
artificial separation, and that the activities can progress in parallel,
expediting software development.  The object-oriented approach supports this
model well.  When the steps are artificially separated, this leads to a large
semantic gap between the steps.  The transformations required to bridge such
semantic gaps are prone to misinterpretation, time consuming and costly.  What
is required in the software industry is a more rational approach.  We should
have learnt from the extremes SA/SD.  This methodology was taken to mean in some
quarters that the methodology was all important, while programming and
programming languages were not at all important.  This attitude was strengthened
by programming languages being arcane and machine-oriented.  A modern software
language will support the integration of the activities of design and
implementation by being readable, and problem-oriented.  A modern language needs
to be as close to the design as possible, as the needs and requirements of an
enterprise can change much more rapidly that the programmers can keep up,
especially in a highly competitive and commercial world. 

So how does C++ fit into this picture? Well it is based on C which was designed
mainly as an implementation and machine-oriented language.  It is an old
language, which did not need to consider the integrated lifecycle approach.  C++
may have some of the trappings of object-oriented concepts, but it is the
marriage of a problem-oriented technique with a machine-oriented language.  It
addresses implementation, but not so well the other aspects of the software
lifecycle.  Since C++ is not so well integrated with analysis and design, the
transformation required to go from analysis and design to implementation is
costly, as the semantic gap between design languages and the implementation
language is great. 

We should have learnt from the structured world that this is not the correct
approach to the software lifecycle.  However, in the OO world we are again
falling into the trap of artificially dividing up the lifecycle into distinct
activities of OOA, OOD and OOP, instead of adopting an integrated approach. 

In summary: C++ provides limited support for design.  Modern languages will
provide a much more integrated approach to the complete software development
process.  C++ includes some of the structures from object-oriented programming,
but does not address the entire software lifecycle, as a modern object-oriented
language can. 


Reusability and Communication
-----------------------------

Reusability is a matter of communication.  In order to use a software component,
you must be able to understand it.  To do so, the writer must communicate the
purpose and intent of the software to the client, and the correct usage of that
software.  In the object-oriented world, clear and concise definition of
software modules is therefore not a mere nicety, but essential if reusability is
to be realised.  Arising out of the issue of reusability is extendibility.  In
order to maximise the reuse of software, it will often need to be tailored to
suit the new application at hand.  The client programmer in this situation must
decide if the software component is indeed suitable for the task in hand, and if
so, what is the best way to extend it. 


Reusability is a matter of Trust
--------------------------------

Reusability is a matter of trust.  If you do not have confidence in a software
component, then it will be difficult to reuse.  Matters for doubt may be, that
it does not provide enough functionality, or even correct functionality, or that
it may not be efficient enough for the purpose, or worse still may crash.  If
the software component cannot be trusted, then it is not a potential candidate
for reuse.  The C/C++ philosophy that checks are not built into the language and
compiler, because programmers can be trusted, works against reusability.  If you
are a potential client of a software component, then you need to be reassured
that the component is as trustworthy as possible. 

Programmers make mistakes, and if you have the uncertainty that certain classes
of mistakes have not been caught by the compilation system, then confidence in
reusability is undermined.  The C/C++ philosophy of trusting programmers is
contrary to the object-oriented goal of reusability.  In the real world of
reusability, the furry headed ideal of trusting programmers is not appropriate. 
In reality, customers doubt the claims of suppliers.  It is the onus of the
supplier to prove their claims, and thus trustworthiness of the software.  The
client is not required to trust the suppliers programmers.  The C philosophy of
trusting programmers is against the commercial interest of both parties. 


Concurrent Programming
----------------------

In the next ten years we will probably be introducing into common use multiple
processor arrays, which will execute concurrent programs.  This requires much
cleaner languages, than the single processor languages of today. 
Object-oriented concepts support concurrent programming, as objects may execute
state changing code independently of each other.  Concurrent programming will be
enabled by the division of the state space of a system into modules to achieve a
high degree of independent processing.  Object-oriented programming provides a
scheme to cleanly divide state spaces.  In traditional sequential programming
practice, the demand that everything be broken down into loosely coupled
modules, that only interact through well defined interfaces may seem
inefficient.  However, it is precisely this scheme that will mean that
concurrent solutions may be developed efficiently and transparently to the
programmer. 

In concurrent programming, a thread in a unit of sequential execution. 
Concurrency is achieved by the splitting of threads.  Threads may be split when
a state changing routine is invoked, but not a value returning function, as the
value must be waited on.  State changing routines may easily be invoked on
another processor.  Object level granularity seems to be a natural candidate for
concurrent processing.  Only one thread may be active in a single object at any
time to avoid simultaneous updates.  Other levels of concurrency are instruction
level, and task or process level.  Task or process level is the level used in
conventional multi-processing systems currently commercially produced, and
instruction level is quite difficult, best being left to instruction pipelines. 

Object level is natural for the programmer, and has the advantage that a
programmer can implement a system without taking into account parallel
processing at all.  The same program will run irrespective of whether the
customer is running a single processor, or a processor array.  Concurrency is
problematic where there is a global environment.  C++ does not preclude the use
of a global environment, and access to shared global data potentially causes a
thread to lock, and where there are many such accesses, the advantage of
concurrency is lost.  Even the introduction of static entities does not help a
great deal, as these still have the problems of sharing associated with
concurrent programming.  It may well prove to be the case that adapting C++ to a
concurrent environment proves to be more technically difficult than the
inclusion of garbage collection. 

In summary: Concurrent programming will require languages which purely express
the requirements of the systems being implemented.  Concurrent programming will
require the simplest and most consistent programming paradigms, and extremely
disciplined modularisation, so that a large state space can be updated
concurrently.  The division of state space into objects as in OOP is essential
if concurrency is to be manageable, and used to the greatest benefit. 
Programmers will be able to produce more powerful systems by the acceptance and
adoption of more 'restricted' practices.  It is up to the compilers and target
architecture to determine the optimal way to produce an executable system. 



The role of Language
--------------------

For a light intermission between sections, there are some interesting points
made by the Cambridge Encyclopedia of Language.  It says that language is an
emotional subject.  "It is not easy to be systematic and objective about
language study.  Popular linguistic debate regularly deteriorates into invective
and polemic.  Language belongs to everyone; so most people feel they have a
right to hold an opinion about it.  And when opinions differ, emotions can run
high.  Arguments can flare over minor points of usage as over major policies of
linguistic planning and education. 

Now while natural language may be difficult to be "systematic and objective
about", does this apply to computer languages? While the definition of natural
language is beyond our control, programming language definition is certainly
within our control.  Programming languages must both have the expressiveness of
natural language, yet be precise and semantically consistent.  As programming
languages have rigorous requirements, we should be even more critical and
objective about them.  Unfortunately, it is a measure of immaturity in the
programming profession that criticism is often met by emotional and irrational
argument.  This leads many to conclude that the choice of a programming language
is no more than a religious issue.  This is incorrect as the choice of a good
programming language should be done on technical merit, and there are technical
measures by which a language may be judged effective or not.  If language choice
is merely a religious issue, then we may as well still be programming in
assembler, or maybe even binary.  Understanding the role of language will help
quantify what it is we have to measure. 

The encyclopedia lists several functions of language.  "To communicate our
ideas", it says is the most common answer, and this must surely be the most
widely recognised function of language.  Other functions it lists are: emotional
expression, for instance, when we stub our toe, we often emit words, even when
there is no one to hear; social interaction, for example if someone sneezes, we
often "bless" them; the power of sound, as in poetry and rhyming jingles etc;
the control of reality, as in spells and incantations, perhaps computer programs
and spells are very close in purpose; recording the facts, record keeping,
historical and geographical documents, etc; the instrument of thought, we quite
often reason about things to ourselves in language; the expression of identity,
it can tell the world who we are, or affirm our belonging to certain groups. 
Perhaps the most important role of computer languages is that of description. 

"Language shapes the way we think, and determines what we can think about." -
B.L.Whorf.  As mentioned before, this is quoted by Bjarne Stroustrup.  But how
true is this? The encyclopedia says, "It seems evident that there is the closest
relationship between language and thought: everyday experience suggests that
much of our thinking is facilitated by language.  But is there identity between
the two? Is it possible to think without language? Or does language dictate the
ways in which we are able to think? Such matters have exercised generations of
philosophers, psychologists, and linguists, who have uncovered layers of
complexity in these straightforward questions.  A simple answer is certainly not
possible; but at least we can be clear about the main factors which give rise to
complications. 

Indeed it seems that there can be both verbal and non-verbal thought.  For
example following a road map in a car.  But when we try to put this into words,
we find it quite a tedious task.  The above Whorf quote is a statement of the
position of the Sapir- Whorf hypothesis on language and thought.  This was
formulated by Edward Sapir (1884-1939) and his pupil Benjamin Lee Whorf (1897-
1941).  It reflects the view of the day that great value was placed on the
diversity of the languages and cultures of the world.  The Sapir-Whorf
hypothesis combined two principles.  The first is 'linguistic determinism',
which states that language determines the way we think.  The second, 'linguistic
relativity', that the distinctions found in one language are not found in any
other. 

The Sapir-Whorf hypothesis in its strongest form, as in the Whorf quote, is not
generally accepted now.  For one reason, it is known that concepts can be
translated from one language into another, even if in one language, the concept
may be expressed in one word, but takes a longer explanation in another.  A
weaker version of the Sapir-Whorf hypothesis is accepted.  That is that
"language may not determine the way we think, but it does influence the way we
perceive and remember, and it affects the ease with which we perform mental
tasks.  Several experiments have shown that people recall things more easily if
the things correspond to readily available words and phrases.  And people
certainly find it easier to make a conceptual distinction if it neatly
corresponds to words available in their language.  Some salvation for the Sapir-
Whorf hypothesis can therefore be found in these studies, which are being
carried out within the developing field of psycholinguistics."

The important question to the programming community is do programming languages
'shape' the way we think about and design systems.  The negative argument
suggests that it is the concept behind the languages that are important, not the
languages themselves, as they only provide a framework for the expression of the
concepts.  A language can only be as good as the concepts it implements, but a
language must also be a simple and clean implementation of the concepts, perhaps
expressing the concepts in the equivalent of as few words and constructs as
possible.  Programmers who understand the concepts should have no difficulty in
adapting to different languages, as long as the concepts are implemented
elegantly in the new language. 

A language may be judged like a wine connoisseur judges wine, by holding it up
to the light to judge it for clarity and colour.  Ultimately, it is the taste
that matters, but good colour and clarity suggests that the taste is more likely
to be good.  So with programming languages, the clarity of the definition of the
language will help that which is important, producing quality software using
that language. 

So where does this leave Sapir-Whorf with respect to programming languages?
Programming languages do not shape the way we think.  It is the concepts that
shape the languages, and it is the way we think that shapes the concepts.  Those
who have attempted to learn a language in order to learn object-oriented
programming realise that it is the concepts which must be grasped in order to be
effective.  Once the concepts are learnt, object-oriented programming seems a
most natural way to program, as it matches most effectively the way we think. 
If C++ has been designed according to the Sapir-Whorf hypothesis, its
philosophical basis is quite at odds with a computer industry that should shape
tools best suited to its purposes, processes, thinking, and concepts. 
 
Quotes taken from "The Cambridge Encyclopedia of Language", David Crystal,
(Cambridge University Press 1987). 





                              Generic C criticisms
                              ====================


These criticisms apply to the C base language, but in general adversely affect
C++. 

Pointers
--------

The C pointer mechanism requires the programmer to be concerned with issues that
the compiler can automatically take care of.  Firstly, the programmer must be
concerned with the underlying address mechanism.  Secondly, the programmer must
be concerned with the correct dereferencing of pointers to access the referenced
entity.  Many languages make both concerns transparent to the programmer, being
naturally the responsibility of the compiler to generate such code, with no loss
of generality, or efficiency. 

An example is that the C programmer has to worry about the means of parameter
passing, and must work out the correct use of &s and *s.  If this is incorrect,
at best a syntax error may be produced, at worst incorrect code will pass into
testing and production.  This information is specified in the parameter
declaration, so that the programmer needs no longer worry about the access
mechanism in the body of the routine.  This again enables flexibility, as the
use of the parameter is implementation independent.  The pointer mechanism of C
means the programmer must constantly be aware of the implementation mechanism. 
This is against the principle of OOP that all data access should be achieved in
an implementation independent manner.  C++ rectifies this to some extent by the
introduction of reference variables (is this an admission that the other
languages were correct all the time?) This is another syntactic variation that
programmers must be aware of. 

Another small problem is the confusion caused by the style of declaration -

	int*	i, j;

This does not mean, as might be easily read -

	int	*i, *j;

but

	int	*i, j;

and should be written thus to avoid confusion.

Another consideration is that the mechanism for implementing dynamic memory
space may change from one target environment to another.  For example on the
Macintosh, relocation of objects is implemented by a double indirection
mechanism (handles).  This is similar to the Unisys A Series mainframe approach
of having all object descriptors access the target object via a master
descriptor, which stores the actual address of the object.  However, the A
Series makes this completely transparent to programmers in all languages. 
Different implementations may not allow relocation at all, and thus the need for
double indirection imposes an unnecessary performance penalty.  Such details
should not be the concern of the programmer, but rather the compiler in the
target environment. 

The manipulation of C pointers is error prone.  A pointer may be incremented
past the end of the entities they are pointing at, with subsequent updates via
the pointer possibly corrupting other entities.  How many lurking and undetected
errors may be in programs because of this? This again illustrates how C
undermines OOP by providing a mechanism where state outside of an object's
boundaries may be changed.  This problem is exacerbated by the fact that
pointers are intrinsic to the method of writing software in C, and the
techniques are so widely used that many programmers are unable to understand how
software can be produced without their use.  Pointers as implemented in C make
the introduction of advanced concepts like garbage collection and concurrency
difficult. 


Arrays
------

As noted on p 137 of the C++ ARM, C arrays are low level, and yet not very
general, and unsafe.  Modern software production has far less dependence on
arrays than in the past, especially in the object-oriented environment.  The
trade off to be optimal, rather than general and safe no longer applies for most
applications.  C arrays provide no run-time bounds checking, not even in test
versions of software.  This seriously undermines the semantics of an array
declaration, ie that an array is declared to be of a particular size, and can
only be indexed by values within the given bounds.  If also compromises software
safety.  Also, in C there is no notion of dynamically allocated arrays, whose
bounds are determined at run time, as in ALGOL 60.  This limits the flexibility
of arrays.  Thus the C definition of arrays compromises both program safety and
flexibility. 

Arrays are in general just another object-oriented entity, and should be treated
in an object-oriented manner as a class of data structure, to which are applied
the interface definitions, and consistency checks inherent in object-oriented
systems. 

D.C.  Ince in ACM SIGPLAN Notices, January 1992, in an article "Arrays and
Pointers Considered Harmful", explains why arrays and pointers need not be
relied upon so heavily in modern software production, as higher level
abstractions such as sets, sequences, etc are better suited to the problem
domain.  Arrays, and pointers may be provided in an object-oriented framework,
and used as low level implementation techniques for the higher level data
abstractions.  As has already been mentioned object-oriented programming is very
useful for the encapsulation of implementation and environment oriented details. 
Ince suggests that arrays and pointers should be regarded in the same way as
gotos in the seventies.  He suggests that languages such as Pascal and Modula-2
should be regarded in the same way as assembler languages in the seventies. 
Perhaps even more so should C and C++ be regarded, as pointers and arrays are
far more intrinsic in the use of C and C++. 


Function Parameters
-------------------

One use of pointers is to simulate by-reference parameters with by-value
parameters.  Being able to distinguish between by-value and by-reference
parameters is not just a syntactic nicety, included in most high level
languages, but a valuable compiler technique.  It means that the compiler can
detect where dereferencing is needed, and can generate code accordingly, without
requiring the programmer to be concerned about this.  However, most languages do
not even get this correct, allowing assignment to the local copy of by-value
parameters.  By-value parameters should be read-only, and not allowed to be the
target of an assignment within a routine.  By reference parameters mean that the
value of a parameter passed to a routine may change.  This introduces a
mechanism of updating the state space, other than straight assignment (although
the routine may use assignment to achieve the 'dirty deed'.) Such value changing
mechanisms introduce side-effects. 

Object-oriented principles suggest that parameters should not be by-reference at
all, whether given direct language support, or simulated support as in C.  OOP
realises that by-reference parameters are another mechanism that allow
side-effects, changing the environment beyond the boundary of the current
object.  Parameters need only be read-only, and if a change is required to an
object passed as a parameter, then it must be performed by calling a routine
encapsulated in that object.  This removes the possibility of the side-effects
of by-reference parameters, and means that an object cannot change the
environment outside of itself by assignment to a by-reference parameter within a
routine.  That an entity may be updated via a point external to itself, is quite
opposite to the data hiding principle of object-oriented programming. 


void *
------

"Passing paths that climb half way into the void" - Close to the Edge, Yes

Is this the programming equivalent of an oxymoron? That you can declare a
pointer to void suggests some sort of semantic nonsense, a dangling pointer
perhaps? While we can have some fun conjecturing what some of the obscure syntax
of C++ may suggest, a serious problem is that void * declarations are used to
defeat the type system, and thereby compromise the advantage of having a type
system.  A well thought out type system requires no such facility. 

When a typed entity is assigned to a reference of void *, it looses all its
static type information.  When it is assigned back to a typed reference the
programmer must explicitly specify to the compiler the type information.  This
should at least result in a run-time check, to make sure that the correct type
actually is being assigned.  Without type checks, the routines of one class may
be mistakenly applied to objects of another class. 


void fn ()
----------

The default type that a function returns is int.  The default should be a
typeless routine, returning nothing.  This is an example of where C's syntax is
not well matched to the concepts and semantics.  Syntactically no <type> should
suggest nothing to return.  In C though the function must be declared void. 
Also a typed function may be invoked independently of an expression.  It is a
shorthand way of discarding the value returned, which means the compiler does
not detect that the function invocation has failed to match the functions
signature.  Thus an important type of consistency check is not made.  Values
should be returned because they need to communicate with the outside world, and
ignoring returned values is often dangerous. 

-- 
Ian Joyner                                     ACSNet: ian@syacus.acus.oz
ACUS (Australian Centre for Unisys Software)   Internet: ian@syacus.acus.oz.au
Tel 61-2-390 1328      Fax 61-2-390 1391       UUCP: ...uunet!munnari!syacus.oz