Understanding the const qualifier

By Carlo Wood carlo at alinoe dot com

The general case

Consider an arbitrary type TYPE.

TYPE x;

The variable x is not const when it is possible to change its value, for instance by assignment:

x = ...;

When the string represented above by TYPE does not contain a reference or pointer, then you can make it a constant by putting a const in front of it:

const TYPE x = ...;

You can declare a constant of any type, by putting const behind it:

TYPE const x = ...;

Notes

Passing by const reference

Another way of getting (to do with) const types is when you pass a reference or pointer type to a function.  Consider:

TYPE x;

(We know that x is not const (and thus also not a reference): it isn't initialized)

foobar(x);	// Call function foobar with parameter x.

The prototype for foobar can have any of the following footprints:

void foobar(TYPE);		// Pass by value
void foobar(TYPE&);		// Pass by reference
void foobar(TYPE const&);	// Pass by const reference

Note that I put the const to the right of TYPE because we don't know if TYPE (this is not a template parameter, but rather for instance a literal char*) is a pointer or not!

The last prototype promises not to change x, and hence it would be ok to pass a constant by reference to this function:

TYPE const x = { ... };
foobar(x);			// Ok for prototype 1 and 3.

When sizeof(TYPE) is larger than the builtin types, people often prefer to pass the object as a const reference (const&), instead of making a copy of the whole object (as prototype 1 would do).

When dealing with pointers, things are similar:

void foobar(TYPE);              // Pass by value
void foobar(TYPE*);             // Pass by pointer
void foobar(TYPE const*);       // Pass by pointer-to-const

A peculiar result of using a reference instead of pointer is (apart from having to type less * and & characters in your source code) that you are allowed to pass temporaries to functions without copying them (as a 'pass by value' would do).  After all, you are not allowed to take the pointer to a temporary and therefore can not use the third prototype (Pass by pointer-to-const) even while the prototype itself promises not to change the temporary.  It is possible to pass a temporary to a function as a reference-to-const though:

void foobar(TYPE const&);
foobar(TYPE());		// Allowed.

Avoiding confusion by using a good programming style

The confusion starts when TYPE itself is a pointer or reference and in particular when it already contains const qualifiers.

For instance:

int x1;			// TYPE is "int"
const int x2 = 3;	// x2 is a constant

int* p1;		// TYPE is "int*"
const int* p2;		// p2 is NOT a constant

Huh? No initialisation needed? No, because the variable p2 is not a constant: It only points to constant data!

If you want to make p2 itself constant then you'll have to write:

int* const p2 = ...;

This is the reason that it is better to put qualifiers always to the right of the TYPE, to avoid confusion:

int x1;
int const x2 = 3;

int* p1;
int* const p2 = &x1;

Note that now we have to initialize p2.

But how to declare a pointer constant that is initialized with &x2 (a pointer to an int const)?

int const* p3;			// pointer to `the type of x2'
int const* const p4 = &x2;	// pointer constant to `the type of x2'

This is very logical, when you realize that the qualifiers work on everything on the left of them, as do * and & in types.

Therefore, put the * for a pointer and the & for a reference, directly against the type on the left of it.  That is better because it makes clear that it isn't an operator* (or operator&), but that it is part of the type.

For instance, int const&* const** means «A pointer to int const&* const*»;  the * in types operates on everything on the left of it.

Important: When you look at a type that contains a * (or a &) always realize that the * (&) works on everything on the left.  This will avoid confusion when people write:

const Foo&

Instead of

Foo const&

In fact, many gurus use the first version: they put a const to the left of types that are not a reference or pointer.  This has a historical reason, the style was born in a very early stage.  Bjarne Stroustrup wrote me:

I don't remember any deep thoughts or involved discussions about the order at the time. A few of the early users - notably me - simply liked the look of
    const int c = 10;
better than
    int const c = 10;
at the time.  I may have been influenced by the fact that my earliest examples were written using "readonly" and
    readonly int c = 10;
does read better than
    int readonly c = 10;
The earliest (C or C++) code using "const" appears to have been created (by me) by a global substitution of "const" for "readonly" ).

const member functions

The const qualifiers of class member functions mean that you are allowed to call that member function even when the object of that class is const.
Thus:

struct A { void m(void) const; };

A const x;

x.m();		// Only ok when `m()' is marked `const'.

Member functions should be marked const if and only if it is garanteed that by calling them the object is not changed.

For example:

class A {
  private:
    A* M_a;
  public:
    A(void) : M_a(this) { }
    A& get_a(void) const { return *M_a; }
    // ...
};

This example shows that even if we return a non-const reference to the very same object (M_a was initialized with this on creation), the function get_a is still constant: It doesn't change the object at all, it just returns the value of M_a (get_a() is an accessor).

Trying to use const for class member functions for other reasons is wrong.  Because the given class allows to change a constant object A we can conclude that the design of the class is wrong.

The following example is correct and shows how to use const for member functions for overloading purposes.

class B { };

class A {
  private:
    B M_b;
  public:
    A(B const& b) : M_b(b) { }
    B& b(void)			// Non-const member function
        { return M_b; }		// because we return directly
				// a non-const reference to
				// to a member.
    B const& b(void) const	// Overloaded for constant object.
        { return M_b; }
};

These two examples are very closely related.  The difference is that in the second case we know first hand that the returned reference is a reference to a member of the class, while in the first example M_a can in principle point to any instance.  Technically there is no difference between get_a and the first b(void), the only difference is the design of the two classes (bad design versus good design).

You can use the following rule of thumbs to decide what to do:

Note that one method to correct the bad design of the first example is to remove the get_a(void) method and add one or more member functions to take over the operations on A that are now apparently performed by the functions that call get_a().  Another method would be to try making M_a a const-pointer (A const*); perhaps the program never uses non-const access.

Valid CSS! Valid HTML 4.0! Copyright © 1999 - 2002 Carlo Wood.  All rights reserved.