From: jcoxhead@acorn.co.uk (Jonathan Coxhead)
Newsgroups: comp.lang.c
Subject: |const| as an operator
Date: 12 Oct 92 13:46:00 GMT
Organization: Acorn Computers Ltd, Cambridge, England


   I've been trying to get to grips with |const|, because I've been writing
a little programme to generate C source. In particular, consider

      int const *const ii = ...;

This declares a constant pointer to a constant integer, but which |const|
refers to which? I thought I'd worked it all out: but then my programme
generated

      int *(const ii) = ...;

which is wrong. (The ISO standard says that |*const| is an operator in its
own right, not further subdivisable.)

   The model in my head was that |const| is an operator, allowable only in
declarations, with the meaning 'this operand is constant,' i e, 'this
operand may not be assigned to.' As I said, this is wrong - but it is
interesting to work through the consequences of this misapprehension. Here
we leave ISO C behind, moving into a dimension not only of sight, but also
of sound ...

   Taking the easy case first

      int const i;

specifies that i is constant: in other words,

      i = ...;

is illegal. That being the case, the declaration should be faulted unless it
has an initialiser:

      int const i = 42;

   Similarly,

      int *const ii = NULL;

says that |ii| is constant, and initialises it - the |const| operator
applies to the |ii|, so that is what is constant. If we declare

      int const *ii;

it is |*ii| that is constant, and this declaration needs no initialiser:

      *ii = ...; illegal
      ii  = ...; legal

Also, after the definition

      char *s = "abcdef";

|s| may be modified, but |*s| may not - so this is equivalent to

      char const *s = "abcdef";

And so we see why

      int const *const ii;

has the effect it does, and why I thought I could parenthesise it as

      int (const (*(const i)));

Arrays
------
   To continue down the same track, what does

      int const a [];

mean? Since |const| is an operator, it binds less tightly than |[]|, so the
meaning is

      int const (a []);

which specifies that |a [x]| may not be assigned to. So |a| is an array
whose contents cannot be changed. It had better be given an initialiser, in
that case, since there's no other way of giving the elements values:

      int const a [] = {2, 3, 5, 7, 11};

A compiler would be able to spot this and give it static duration, even if
it was declared inside a recursive function.

   The other possibility for arrays is

      int (const a) [];

which says that |a| is constant. This is redundant, since arrays cannot be
assigned anyway.

Functions
---------
   This is where it gets interesting. Given

      int f (int);

the normal situation is that both

      f = ...;

and

      f (x) = ...;

are illegal - there are no function variables in C, and function calls are
not l-values. So that declaration is equivalent to

      int const (const f) (int);

Again, this parses with the function application binding more tightly than
the "operator" |const|:

      int const ((const f) (int));

   But if we have a function |f| that returns an |int *|,

      int *f (int);

we normally can write

      *f (x) = ...;

To disallow that possibility, we could declare

      int const *f (int);

   Considering function pointers, there are again two useful possibilities:

      int (*const ff) (int) = &f;

specifies that |ff| points to |f|, and will not be changed hereafter;
whereas

      int (*ff) (int) = &f;

makes no such assertion - but it is equivalent to

      int const (const *ff) (int) = &f;

for the two reasons above.

Structures
----------
   Given a declaration

      struct {int x; int y;} s;

we can write

      s.x = ...;
      s.y = ...;

but if we say instead

      struct {int x; int const y;} s;

we have disallowed

      s.y = ...;

only. This is another declaration that needs an initialiser in order to make
sense. And

      struct {int x; int y;} const s = {1, 2};

prohibits both.

Const as a storage-class specifier
----- -- - ------------- ---------
   A lot of people write |const| on the other side of the type keyword,
where it can't syntactically be an operator:

      const int ... x ...;

The interpretation of this is that it means the same as

      int const (... x ...);

rather than

      int ... (const x) ...;

which you might expect - after all, a |static| specifier applies only to the
name it was given, not to the whole expression, so it would make sense if
|const| did too. However, that is not the ISO definition.

Volatile
--------
   Volatile can be thought of in a similar way syntactically, but with the
semantics 'expressions involving this operand are to be treated in the
standard-defined way.' To take a function case, given

      int *f (int);

- clearly, any expression involving |*f (x)| also involves |f (x)|; and any
expression in |f (x)| involves |f|. So there are three possibilities:

      int *(volatile f) (int);
      int *volatile f (int);
      int volatile *f (int);

The first of these prevents optimsation of |f|, |f (x)| and |*f (x)|, and
is equivalent to any of

      int volatile *volatile (volatile f) (int);
      int *volatile (volatile f) (int);
      int volatile *(volatile f) (int);

The second retains the possibility of optimisation of expressions in |f|
only, while the third prohibits optimisation of |*f (x)| only.

Parameters
----------
   The specifiers |const| and |volatile| have no effect in formal parameter
specifications, and so are to be disregarded. In actual parameter lists,
they have the usual meanings: so the declaration

      int f (int);

matches the definition

      int f (int const x) {return x;}

The |const| here is internal to the definition of |f|: it means that the
body of the function won't try to change the value of the formal parameter.
This is sometimes regarded as poor practice, anyway.

   This applies to |const| and |volatile| as the final qualifier only: the
declarations

      int *i;
      int const *j;

do result in different types: the |i| can point to anything, but |j| can
only point to constants - so

      int f (int *);
      int g (int const *);

also result in different types.

   As an example of everything so far, if there are 5 hardware devices at
the words from 0xFFFFFF00 on our computer, we can declare this (slightly
redundantly) as

      int volatile (const M) [5] = 0xFFFFFF00;

since M (the address) is constant, and expressions in |M [0]|, ..., |M [4]|
are not to be optimised.

Summary
-------
   Does this interpretation of |const| and |volatile| have any Standard
support? I suspect that what I have outlined is a strict superset of what
ISO C currently allows: in other words, it offers the same interpretation
for programmes which are currently legal, but allows other constructs that
are not at present permitted, additionally given them useful semantics.

   I expect it contravenes some parts of C++, since that language may
already have used |const| in structure (or class) declarations to mean
something else.

   There are some things I have glossed over, but I hope what I have said is
an interesting way of looking at these qualifiers. Any comments?

   /|
  (_|/
   /|
  (_/