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

(C++ Critique Part 4)

fn ()
-----

Empty parenthesis is the invocation operator in C.  This relates C to languages
such as FORTRAN and COBOL, where the specific CALL and PERFORM instructions were
required to indicate routine invocation.  This similarity of C to FORTRAN and
COBOL is not so obvious, as the () operator is more mathematical looking in
nature.  In the ALGOL style of languages, invocation is automatically deduced by
the compiler when it sees a routine name, as it knows that the identifier refers
to a routine.  This compiler technology was not realised at the time FORTRAN and
COBOL were developed.  This compiler technology is possible, because much
information is stored by the compiler about the entity, and the compiler can
check that subsequent use by the programmer is consistent with the declaration,
and in many cases the compiler can be relied upon to generate correct code,
without having to burden the programmer with having to redundantly supply this
information.  This contributes to flexibility and implementation independence. 

A specific invocation operator results in an artificial distinction between
functions and variables.  Indeed, a variable used in an expression is a value
returning function, it just returns the value stored in the variables memory
location, rather than calling a function to compute the value.  Functions and
variables are the implementation viewpoint of the problem-oriented concept of
attributes.  Attributes may be both static, and dynamic.  For example, a printer
may have static attributes like pages per minute, printer type (laser, line,
etc).  A printer may also have dynamic attributes, like the number of print jobs
queued, pages left in the current job, etc.  However, the user of the attributes
should not be concerned, whether they are static or dynamic, just that the
interface returns a value of a particular type.  This view also ensures
implementation independence. 

The important semantic information of an identifier to the programmer is that it
returns a value of a given type.  The programmer should not care whether the
value is the result of a routine, or the value stored in a memory location. 
What is important is that the value is of a type compatible with the expression
in which it occurs.  This can be checked by the compiler. 

The function invocation distinction reduces flexibility.  If for optimisation, a
function is replaced by a precomputed variable, it is not possible to use the
same name without removing all the () from the original function invocations. 
This may be spread over many files, and such optimisation may not be attempted
by the programmer to avoid the tedium associated with the task.  Thus
implementation detail is visible for the outside world to see. 

If the rarer circumstance of needing a reference to a function is required, then
this special situation should have the extra explicit syntax, not the common
invocation situation.  Having such function pointers though is analogous to the
call by name facility in ALGOL, and this was recognised as having pitfalls.  All
these pitfalls may be avoided by consistent application of the object-oriented
paradigm. 

A common use of function pointers is to explicitly set up jump tables.  The
mechanism behind virtual functions is a jump table of function pointers, and the
design of a program may take advantage of this fact, without resorting to
explicit jump tables.  Another use is to jump to a function in a table, which is
indexed by an input character.  This mechanism is better catered for by a switch
statement, which makes what is meant explicit, while keeping underlying
mechanisms (and possibly optimisations) implicit.  C++ allows function pointers
to member functions to be stored in tables (via the .* and ->* operators).  This
though cannot take advantage of dynamic dispatch through the virtual jump table. 

In summary: Mathematically, any entity which returns a value of a given type is
a function.  This includes variables used in expressions, so distinguishing
between variables and functions introduces an artificial distinction into the
language.  This reduces flexibility.  Distinguishing between static and dynamic
attributes, reduces implementation independence.  The () operator is equivalent
to the FORTRAN CALL, and COBOL PERFORM, only the syntax is different.  How to
handle an entity should be left up to the compiler, as the programmer has
already specified the intended usage by the nature of the entities declaration. 


++, --
------

These are another unnecessary shorthand convenience.  The shorthand += and -= is
more powerful, as the variable may be incremented by values other than 1.  This
illustrates that there are no less than three ways to perform the same thing -

a = a +1
a += 1
a++
++a

For full generality, only the first form is required, the others are a mere
convenience.  If it is mistakenly believed that the other forms produce more
optimal code, then it should be pointed out that code generators, especially for
expressions, can produce the best code for a target architecture.  The last two
forms a++ and ++a are the postfix and prefix forms.  They are often used in the
context of another expression.  Thus one expression may be used to perform
several updates.  This is a very powerful and convenient feature, but introduces
side effects into an expression, which sometimes have surprising effects, and
may lead to program errors.  The following example is given on p.46 of the C++
ARM -

	i = v[i++];	// the value of 'i' is undefined

This example the ARM points out should be detectable by compilers, but the exact
interpretation appears to be left to the implementation, which may contribute to
non-portability. 

It is better to program in a straightforward manner, as an optimising compiler
will optimise well beyond what a programmer can do.  An optimising compiler will
analyse the surrounding code, and if an entity is used several times in a local
scope, it will keep the value of that entity handy locally at the top of a
stack, or in a register, rather than retrieve it from main slow memory several
times.  The nature of such optimisations depend on the machines architecture,
which a programmer should not be aware of, or expected to be aware of, as open
systems demands that programs should be able to be ported amongst diverse
architectures and environments, very different to the original machine. 

As with name overloading, memory storage update is a problematic, but necessary
part of programming.  It should be provided by a language in a consistent and
expected way.  Most languages have realised that memory update is problematic,
and typically only provide limited but sufficient ways of updating, by an
assignment operation.  (Many languages also have block memory copies as well,
but block copy could also be provided by assignment.) Furthermore, most
languages avoid side-effects by limiting updates to only one per statement.  C
provides memory update in too many ways.  These ways add nothing to the
generality of the language, but do add ways that program errors may be produced. 

In summary: Increment and decrement operators introduce no added benefits for
optimisation with respect to modern compilation environments.  These can
introduce side-effects by multiple updates within one expression.  These
operators introduce no extra generality or power of expression into the
language. 



Defines
-------

The define declaration -

#define d(<parameters>)

has a different effect to -

#define d  (<parameters>)

The second form defines d as (<parameters>).  Extra white space between tokens
should not affect semantic meaning of constructs. 


Case Distinction
----------------

While it is good to adopt some typographic convention for the presentation of
names the fact that C distinguishes between upper and lower case in names can
cause confusion.  Whether a character is written 'i' or 'I' it remains the same
character.  We would not make the distinction if the i were italicised or
emboldened.  Case distinction is a weak form of name overloading.  If you can
overload names in such a way, you can easily use the wrong one.  For example if
an identifier is declared Fred, another one may be declared fRed.  Such names
may easily be mistyped or confused.  Typographic conventions should not affect
the semantics of a program (consider RPG). 

As every programmer will have experienced, one character errors are more
difficult to detect than one would think.  We are in general not good
proof-readers.  There is a psychological reason for this that the the brain
tends to straighten out errors for our perception automatically.  The human
brain is an excellent instrument for working out what was intended, even in the
presence of radical error.  In order to overcome this programmers have to use
their powers of concentration to override this natural tendency of the brain. 
Distinguishing upper from lower case in names only adds another level of
difficulty.  Modern language design takes into account such psychological
considerations in these small but important details, being designed towards the
way the human brain works, not towards the way computers work.  Such
considerations make a big difference to the effectiveness of people, but do not
have any impact at all on the efficiency of code generated for the computer. 
What is more important, people or computers?

As has been suggested, case distinction provides another form of name
overloading.  Name overloading is a double-edged sword, as it is the attachment
of different semantics to the same name.  This opens up the possibility of
confusion, but it is difficult to imagine programming without it, and its
corresponding notion of scope.  In object-oriented programming, name overloading
is used in a very powerful, but disciplined and restricted manner in order to
limit the problems and confusion that are inherent to name overloading.  Case
distinction can result in unexpected name overloading that can cause confusion
and errors.  Name overloading as has been suggested in the section on name
overloading should only be provided in controlled and expected ways.  This means
that where a name is overloaded in the same scope that the compiler should
report an error, or require the programmer to explicitly state that this is the
intention, as with Object Pascal's override. 

As another example, a commonly used technique is -

class obj
{
	int	Entry;

	void	set_entry (int entry)
	{
		entry = Entry;
	}
}

Upper and lower case distinction also illustrates another old compromise that C
made.  The compromise was that upper and lower case letters are represented by
distinct codes in the ASCII character set, and to remove this distinction
required precious computer time.  Most languages overcame this problem by an
even worse restriction, that everything had to be in upper case.  C's mix of
upper and lower case seemed good in comparison, but it was still not correct. 
These old languages and C considered processor time to be more important to
convenience for people.  But 20 years ago perhaps it was, as computers were
expensive and slow.  This assumption has been removed a long time ago, but we
are still stuck with it in C based languages, and the Unix operating system. 
Early compilers also had restrictions (original definitions of C say that only
the first 8 characters of identifiers were significant), on the length of
identifiers to between six and ten characters, and this meant that the upper
lower case distinction gave the benefits of an extended character set.  This
consideration no longer applies in all but the most basic compilers. 

As an aside note, on a typically common practice in any language, due to
historically short identifiers programmers shorten identifiers by the omission
of vowels.  This leads to error and ambiguity, and difficulty in reading and
interpretation at a later date.  For example do the characters 'prvt' represent
'private' or 'pervert'? This slows good typists down, and poor typists probably
even more, while they work out exactly which vowels to miss out.  This shows
that many programmers have not adapted their practices as the limitations of
technology have been removed. 

In summary: The language designer has two alternatives.  To either have case
distinction or not.  Case distinction can lead to rare but difficult to detect
errors, due to yet another form of name overloading.  The prohibition of case
distinction may occasionally force the programmer to have to choose an
alternative identifier, but the fact that a potentially erroneous name clash has
occurred will have been detected early.  The event of error from case
distinction may be very rare, but then again would you trust your space shuttle
to even the remotest chance of something going wrong, where billions of dollars
are at risk?


Assignment Operator
-------------------

Using the mathematical equality symbol for the assignment operator is an example
of a poor choice of symbols.  Programming assignment is not equal to
mathematical equality (:= != =).  Language designers of ALGOL style languages
realised they were semantically quite different, so took the care to
distinguish, only using '=' to mean equality in the mathematical sense.  In C
the lack of distinction leads to error.  It is easy to use = (assignment) where
== (equality) is intended, and the result looks quite reasonable, resulting in
errors that are difficult to detect. 

In fact this leads to a more general criticism of C, in that it has a pseudo
mathematical appearance.  There are very few people who are proficient at the
interpretation of mathematical theorems, most passing over such sections in
text, unfortunately often making the assumption that if the mathematics is
there, the surrounding text has been proved correct.  Generally, the pseudo-
mathematical nature of C shares this bad attribute of mathematical notation as
it is difficult to read, while lacking the semantic consistency and precision of
mathematical notation.  One of the keys to reusability is readability. 


Type Casting
------------

Type casting is just a specific form of mathematical function, which maps values
of one type onto values of another type.  Type casting has been useful in
computer systems, as often, it is required to map one type onto another, where
the bit representation of the value remains the same.  Type casting is therefore
a trick to optimise certain operations.  Type casting provides no useful concept
that cannot be implemented by more general functions.  Furthermore, type casting
causes problems in strongly typed systems, as it compromises the type system. 
In many languages, type casting is necessary, as the type system has not been
consistently defined, so programmers often feel they have the need of the type
casting mechanism, and may find it difficult to imagine languages where type
casting is not required at all. 

Mathematically, all functions can be regarded as having a type casting function. 
An example often used in programming is to cast between characters and integers. 
Type casts between integers and characters may easily be expressed
mathematically using abstract data types (ADTs). 

TYPE
	CHARACTER

FUNCTIONS
	ord: CHARACTER -> INTEGER	// convert input character to integer
	char: INTEGER /-> CHARACTER	// convert input integer to character

PRECONDITION
	// check i is in range
	pre char (i: INTEGER) = 0 <= i and i <= ord (last character)

The notation '->' means every character will map to an integer.  The partial
function notation '/->' means that not every integer will map to a character,
and it must be specified exactly which subset of integers will map to characters
by use of a precondition.  Now this may be provided consistently in
object-oriented syntax with member functions on a class:

	i : INTEGER
	ch : CHARACTER

	i := ch.ord	// i becomes the integer value of the character.
	ch := i.char	// ch becomes the character corresponding to the value i.

but a routine char would probably not be defined on the integer type so this
would more likely be:

	ch.char (i);	// set ch to the character corresponding to the value i.

Now such basic data types, as character and integer, are usually catered for
well by the hardware of most machines, and it is entirely possible that a
compiler will generate code that is optimal for any target hardware
architecture.  However, such basic data types may be consistently and elegantly
treated in the object-oriented paradigm, by the implicit definition of their own
classes. 

Another example of type conversion is from real to integer.  Here though, the
programmer may wish to specify the use of two type conversion functions to
truncate or round. 

TYPE
	REAL

FUNCTIONS
	truncate: REAL -> INTEGER
	round: REAL -> INTEGER

	r: REAL
	i: INTEGER

	i := r.truncate	// i becomes the closest integer <= r
	i := r.round	// i becomes the closest integer to r

Again many hardware platforms provide specific instructions to achieve this, and
an efficient object-oriented language compiler may indeed generate the code best
suited to the target machine. 

In summary: Type casting compromises the advantages of type safety.  Type
casting in a pure object-oriented system is not needed, as it is only a specific
implementation of the more general mathematical notion of functions. 


Semicolons
----------

A program is a list of elements, and the executable part of a program is a list
of sequentially executed instructions.  Languages such as FORTRAN separated
instructions by requiring that they be placed on different lines or cards. 
Elements in a list need to be separated in some manner, and the semicolon is one
syntax for the separation of the elements in the list.  The semicolon is
therefore part of the syntax of the list, not part of the syntax of the
individual instructions.  But C makes the semicolon part of the syntax of each
instruction by making the semicolon, a terminator of individual instructions. 
This elevates an unimportant semicolon to have a much higher syntactic value
than it should.  Imagine if the comma was a terminator, then function
invocations would have to look like:

	fn (a, b+c, d, e,);

C's handling of the grammar of semi-colons leads to an irregularity in
if/else's:

	if (condition)
		statement1;	/* Semicolon required */
	else
		statement2;

	if (condition)
	{
		statement1;
	}	/* Semicolon must be omitted */
	else
		statement2;
	
This is an irregularity, as both of the above will be reduced by a parser to the
grammatical form:

	if (condition) statement else statement





Conclusions
-----------

C++ is overly complex.  Object-oriented languages should provide sophisticated
concepts in the simplest possible framework.  Where the framework is not simple,
the concepts tend to be lost, and there are many issues that OOP addresses in
order to facilitate the production of complex and sophisticated programs.  Many
of these issues are addressed in implicit and subtle ways, but are lost in C++. 
There are many potential ways of introducing subtle errors into C++ software,
and furthermore, the combination of these will cause even further problems. 

Programming is the orchestration of change within a large state space. 
Object-oriented techniques provide a method of simple division and management of
such state spaces.  To manage such state spaces requires the simplest
techniques, in order to guard against detectable inconsistencies which may lead
to errors in executable systems.  C and C++ do not implement the simple
management of a large state space, and also allow to many potential errors to go
undetected.  The role of a language as a tool cannot seriously be regarded as
some authoritarian that stops us doing what we want or need to do, as many
languages with type safety a consistency checks are often viewed.  Programming
languages should embody the collective wisdom of common sense practices, which
have been learnt over many years, by common and painful experience.  C++ lacks
the implementation of much of this wisdom. 

It is better to detect and avoid errors than to fix them.  The fixing of errors
happens many times during the development process.  This slows the development
process down, and is therefore costly.  Good programmers in this context (often
labeled 'gurus'), are those who recognise symptoms, and recommend fixes.  Good
programmers in the better sense (often labeled 'impractical idealistic
dreamers') adopt better practices (programming languages being a subset of
these), that avoid error in the first place. 

The view that correctness checks are training wheels for students needs to be
dispelled.  Many disciplines have techniques to ensure correctness.  For
example, the metronome in music is not just for students, but will help an
advanced musician ensure that the tempo of a piece is correct, and since playing
to a metronome is more difficult, will help sharpen the musicians performance of
the piece.  The musician does not just view the metronome as a teaching aid, but
as a tool that helps produce a polished and professional performance. 

This paper has shown many cases where C++ uses old C mechanisms to provide
things that can and should be expressed consistently within the object-oriented
paradigm.  For example type casting.  It is therefore suggested that the move to
pure object-oriented languages will indeed facilitate more consistent
programming, which will avoid many errors which typically occur in software
production.  The amount of change required in C++ to address the issues raised
in this paper is seen as largely insurmountable. 

A programming language is just a tool, in the same way that an axe is a tool. 
In chopping down a tree, if the axe is blunt, then procedures, processes and
methodologies may be invented to make the blunt axe as effective as possible. 
However, that does not solve the real problem that the axe that does the real
work is blunt.  So it is with programming languages.  If a system is to be
realised, then it must be implemented, and a programming language is the tool
with which the real work is done.  If the language is blunt, then procedures,
processes and methodologies may alleviate that situation, but they do not solve
the cause of the problem.  Once the axe is sharpened, then real progress may be
made, and the procedures, processes and methodologies also become more
effective.  A good axeman will have good axe wielding technique, but given a
choice of axes will choose the sharpest implement.  A poor axeman may not be
effective with even a sharp axe, but that does not mean that the axe maker will
not bother to produce the sharpest axe for the good axeman.  The argument that
poor programmers will produce bad programs in any language so we shouldn't
bother with better languages is fallacious. 

The critique began with certain questions, and as no work can be absolute
(particularly a programming language), it will end with more questions, which it
is hoped will create more debate, and more questioning into what we are really
trying to achieve with program development. 

Does C++ provide effective communication between programmers separated by both
space and time? Does C++ provide communication between the levels of analysis,
design, implementation and maintenance?

Are the compromises made by C and C++ still relevant to today's environments,
and the environments of the not very near future?

Could C++ be regarded as the PL/1 of the object-oriented world, as PL/1 was the
marriage of FORTRAN and structured ALGOL concepts, and C++ is the marriage of C
with object-oriented concepts?

Are the compromises made for the restricted machines and environments of 20
years ago still appropriate for today? Are language based on 20 year old
compromises appropriate in modern software development environments?

Should new software developments be forced to accept such compromises?

Is C++ patching old material with new cloth, or pouring new wine into old
wineskins?

What are we really trying to achieve in programming anyway?

Ian Joyner
April 1992


-- 
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
Newsgroups: comp.lang.c++