The Mutable Comprehension of const
constas a modifier of pointer/reference types in C and C++ can cause confusion.
I don't know about you, but I get all future-shocked when trying to express or understand concepts where language is used ambiguously. Unfortunately, even after all these years, it seems that the use of
const as a modifier of pointer/reference types in C and C++ can cause confusion.I've been working with some talented C++ developers recently, and have been surprised to learn that confusion over the use of the
const modifier, when applied to pointers and references, still persists even among programmers of considerable experience. I think there are three reasons why this is so: language, syntax, and the C++ Standard.
In my opinion, the key to surviving the ambiguity of
const pointers and references is to not use the word "const" at all, and instead rely on the use of precise nomenclature about what is actually implied by its use: mutability.
The problem with "const" is that it's just not specific enough. When someone says "const pointer" it's not clear whether the pointer is "const" (i.e. it must always points to the same location) or the thing it points to is const(ant) (i.e. the variable to which it points cannot be changed).
This is further complicated by the fact that a variable referred to by a "const" pointer may be changed by another (non-const) alias to it elsewhere in the program. It's actually impossible and inappropriate to infer anything that a variable is
const from a pointer to it.
Rather than this imprecision and ambiguity, I suggest programmers should eschew phrases such as "const pointer", "non-const pointer", "pointer to const
int", "pointer to non-const
int", and "const pointer to const
int". Prefer instead the unambiguous phrases "immutable pointer", "mutable pointer", "non-mutating pointer to
int", "mutating pointer to
int", and "immutable non-mutating pointer to
int". A pointer may or may not be mutable (whether the pointer may be changed to point to a different location) and may or may not be mutating (whether it may be used to change the variable to which it refers).
(Note: in this way, the always-confusing terminology of the "const reference" goes away. Strictly speaking, a reference is always const, because references cannot be reassigned. In the new terminology, references are immutable. Which we knew. Now we need only concern ourselves with whether a reference is mutating or non-mutating.)
C and C++ are agnostic about where the
const modifier can be placed in a majority of contexts. The following pairs of constructs are identical:
// 1. int const int i = 10; int const j = 20;
// 2. pointer-to-int const int* p = &i; int const* q = &j;
// 3. reference-to-int const int& p = i; int const& q = j;
I prefer the second form of each, and I strongly recommend it to C/C++ programmers, because (I believe that) it's the only way in which one can sensibly read composite types, by reading from right-to-left. Consider the following two equivalent ways of specifying a pointer:
const int* const p = &i; int const* const q = &j;
q are immutable non-mutating pointers to
int. If we read from right-to-left, the presence/absence of
const before (i.e. to the right of) the
* determines whether the pointer is mutable/immutable, and the presence/absence of
const after (i.e. to the left of) the
* determines whether the pointer is mutating/non-mutating. By always preferring the
X const form over the
const X form, the
const, if present, will be immediately adjacent to the
*. Doing the same for non-pointer/non-reference variable declarations follows for reasons of consistency.
Let's do a few more to practise:
// A mutable mutating pointer to int int* p = &i;
// A mutable non-mutating pointer to int int const* p = &i;
// An immutable mutating pointer to int int* const p = &i;
// An immutable non-mutating pointer to int int const* const p = &i;
// An (immutable) mutating reference to int int& r = &i;
// An (immutable) non-mutating reference to int int const& r = &i;
The C++ Standard
Historically, the C++ standard is not much of a friend to us in so far as precise names go, and that is also the case in this regard. There are the well known obvious ambiguities, such as the inconsistency between
empty() (an interrogative: is the instance empty?) and
erase() (an imperative: clear the instance contents). With member types of pointers, references, and iterators, the use of the word
const as part of essential member types
const_pointer, and, particularly,
const_iterator, gets more confusing.
It's not only possible to change a
const_iterator, it's actually essential to traversing ranges. Furthermore, with some containers (such as C++0x sets), erasing elements is done via a
const_iterator is not an immutable iterator, it's a non-mutating iterator: an iterator through which one may access an element in a non-mutating manner, and with which one may traverse the sequence to access, in a non-mutating manner, other elements in the container/collection.
Language, Syntax, The C++ Standard: How to Survive
My way to survive these issues are:
- Syntax: always apply the
constmodifier after the type being modified. I've been an adherent to this for pointers and references for many years, but have only recently gone the whole hog in applying it to all types of variables consistently. I'm finding it beneficial.
- Language: when writing documentation or articles (or blogs, or books), I always refer to pointers/references/iterators in terms of their being mutable/immutable and mutating/non-mutating
- The C++ Standard: the fat lady has sung on this one. Don't bother trying to create container types with a mutating called
empty(), or with
mutating_iteratormember types. There's just too much mindshare and technical momentum to buck it.