Topic : A Critique of C++
Author : Ian Joyner
Page : << Previous 13  Next >>
Go to page :


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.  This applies
even more to C and C++, because pointers and arrays are far more
intrinsic in the use of C and C++.

   I agree with Ince that we have less dependence on arrays, and that
pointers in programming languages can be considered harmful.  But I
disagree in as far as the concept of array is useful for mapping one set
of values onto another, where this mapping cannot be described
computationally, but can only be expressed by pairs or tuples of
coordinates.

  

6.3.  Function Parameters

   Parameters are used to pass routines simple values (by-value
parameters), or references to entities (by-reference parameters).
Parameters are inputs to routines, and should not be changed.  When
memory was expensive, reusing parameter space could conserve space.
Changing parameters, however, is semantic nonsense, and most languages
get this wrong.

   By reference parameters enable a routine to change the value of an
entity external to the routine.  Such updates beyond the environment of
a routine are side-effects.  This introduces a mechanism of updating the
state space, other than straight assignment (although the routine can
use assignment to achieve the 'dirty deed'.) The danger is that the
state of an object can be changed without using the well defined
interface of the object.  By-reference parameters should not be used to
change the external world.  Values should only be passed to the external
world by the return value of a function.  Semantically, this is quite
different to assignment to a reference parameter; data flows through the
program in one direction, in via parameters, and out via return values.
Mathematically this maps compositions of inputs to outputs.  Abstract
data types can be used to design such systems.  Also this will help
target environments to increase parallelism and concurrency in a way
transparent to programmers.

   In object-oriented programming, by reference parameters are used to
pass the original object, not a copy.  The called routine, however,
cannot change the state of the referenced object.  Only calling a
routine in the objects interface can change the state.  This has the
desired effect of the object being given to you, without being yours to
change, although you can effect change in the object.

   C shares faulty parameters with many other languages.  The
interaction of C's pointer mechanism with a faulty parameter mechanism,
however, makes C considerably worse than most other languages.  In C,
pointers are used to simulate by-reference parameters with by- value
parameters.  The programmer must perform tedious bookkeeping by
specifying *s and &s for referencing and dereferencing.  Distinguishing
between by-value and by-reference parameters is not just a syntactic
nicety, included in most high level languages, but a valuable compiler
technique, as the compiler can automatically generate the referencing
and dereferencing, without burdening the programmer.

  

6.4.  void *

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

   Is void * the C equivalent of an oxymoron? A pointer to void suggests
some sort of semantic nonsense, a dangling pointer perhaps? Maybe we
should tell the astronomers we have found a black hole! While we can
have some fun conjecturing what some of the obscure syntax of C
suggests, a serious problem is that void * declarations are used to
defeat the type system, and so compromise its purpose.  A well thought
out type system does not require such a facility.  In an object-oriented
type system, the root class of the inheritance hierarchy provides the
equivalent of void.

   When a typed entity is assigned to a reference of void *, it looses
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 is error prone and 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 can be
mistakenly applied to objects of another class.

  

6.5.  void fn ()

   The default type that a function returns is int.  A typeless routine
returning nothing should be the default.  Instead this must be specified
by another confusing use of void.  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.  Also a typed function can
be invoked independently of an expression.  This is a shorthand way of
discarding the returned value.  Values should be returned because they
need to communicate with the outside world, and ignoring returned values
is often dangerous.  In other words, using a typed function as a void
should result in a type error.

   In fact there should be no such thing as a void function.  A void
function is a procedure.  Procedures and functions should be
distinguished.  This distinction belongs to the problem 'what' domain.
A procedure is a routine that changes the state of its object, but
returns no value.  A function should, in general, not cause any change
to the state of an object, but just return some result dependent upon
the objects state.  Mathematically, a function is an entity that returns
a value of a given type.  Procedures are untyped, and do not return a
value.  So it is incorrect to regard procedures as functions.  Functions
as will be explained below have more in common with variables than
procedures.  Procedures can cause side effects, functions should not
cause side effects.  These distinctions are useful when considering
concurrency.

  

6.6.  fn ()

   Empty parenthesis represent the function invocation operator in C.
Even though '()' is mathematical looking, it is semantically equivalent
to FORTRAN's CALL, COBOL's PERFORM, and JSR in assembler.  The design of
these operators was influenced by the underlying machine architectures.
The invocation operator is low level, machine and execution oriented,
and in the 'how' domain.

   This is opposite to most Unix shells, where invocation operators such
as 'run' and 'exec' are not needed.  The ability to execute file names
as commands extends the command repertoire.  The shell runs executables
and interprets shell scripts.  There is no distinction as far as the
shell user is concerned.  This is a widely accepted as an elegant and
effective convenience.  C's () operator introduces the equivalent of a
run command into the language.

   No invocation operator exists in the problem oriented domain of high
level languages.  This is because the semantics of a function is to
return a value of a given type.  How this value is computed is
unimportant.  The value could be computed by a routine invocation, by
sending a message across a network, by forking an asynchronous process,
or by retrieving a precomputed result from a memory location, ie a
variable.

   The distinction that languages like C make between variables and
value returning routines is artificial.  It could be pointed out that
variables are fundamentally different to functions, as you can assign
values to variables.  Functions, however, are the target of assignment.
The return statement accomplishes this in C.  Algol has no return
statement, but uses assignment to the name of the function.  The
assignment of a value to a variable sets the return value for subsequent
invocations of that function.

   It is trivial for a compiler to realise this transparency of view for
variables and functions.  In ALGOL style languages, the compiler
automatically deduces invocation when it sees a name that was declared
as a routine, rather than a variable.  The compiler knows that the
identifier refers to a routine.  This compiler technology was not
realised when FORTRAN and COBOL were developed.  This compiler
technology is possible, because the compiler stores much information
about an entity.  A compiler can check that the programmer uses the
entity consistently with the declaration.  A compiler can generate
correct code, without burdening the programmer with having to
redundantly use an invocation operator.  This enhances flexibility and
implementation independence.

   In fact the Unisys A series architecture elegantly achieves this
level of transparency at the hardware level.  The value call (VALC)
operator loads a value onto the top of stack.  When VALC hits a data
value, that value is retrieved from memory and loaded onto the stack.
When VALC hits a program control word (PCW), a routine that computes the
value to be loaded onto the stack is invoked.

   Variables and functions should be interchangeable for programmer
optimisation.  In C, it is not possible to change a function to a
variable without removing all the ().  This might be spread over many
files, and the programmer might not bother with optimisation to avoid
the tedium of the task.  So the () operator reduces flexibility.  Thus
implementation detail is visible for the

Page : << Previous 13  Next >>