Home : C++ : The C++ Keyword const

Const Correctness

Much of C++ is about ensuring that you (or people using your libraries) make as few mistakes as possible. One of the techniques available to acheive this aim is "const correctness". This article attempts to explain what is meant by the term and how and why it should be used, as well as the practical considerations of writing const correct code.

What is Const Correctness?

In C++ both variables and methods can be constant ("const" is the C++ keyword used to do this). If a variable (or function parameter) is constant, its value cannot change over the variable's lifetime; whilst if a member function is constant, it can be applied to a constant object.

The following example should make this clearer:

class Cmy_class {
private:
    int m_iMyValue;
public:
    int GetMyValue() const;   // A constant method
    void SetMyValue(int iNewValue);   // A non-constant method
};

void main() {
    Cmy_class notConstObject;

    int x = notConstObject.GetMyValue();   // OK
    notConstObject.SetMyValue(2);   // OK

    const Cmy_class constObject;

    int x = constObject.GetMyValue();   // OK
    constObject.SetMyValue(2);   // Compile error
};

The example above shows a very common occurrence in C++ (and OO programming in general) where get and set accessors are used to hide abstract private data. Since the function GetMyValue() does not modify the data, it is declared constant. The SetMyValue() function does change the object, so it should not be declared constant.

Why Use Const Correctness?

The reasons are many and compelling, read on...

Write Good Code and Feel Proud

As I mentioned above, the main reason for writing const correct code is to prevent disasterous mistakes during the implementation and maintenance phases of a project. By declaring a variable or method as constant, the class designer is making an assertion regarding the way that the class is to be used.

During testing a bug may appear which you may think can be quickly and easily fixed by putting a side-effect into a constant method (e.g. the GetMyValue() function above) but if the rest of the class library assumes that this is not the case many new bugs can be introduced to the detriment of the project. Const correctness is a way of making you think about this stuff instead of just hacking in a misguided attempt to meet a deadline.

There are other good reasons to use const correctness:

Improved Efficiency

Function parameters in C++ are copied before calling the function because the function may modify those parameters. However, if a function parameter is declared as constant, it cannot be modified so there is no need for a copy. Whilst copying an integer is a relatively low-cost operation, copying heavyweight classes is not, therefore considerable time can be saved in this way.

Of course, a class passed to a function as a parameter should be useful to that function, otherwise why pass it at all? Therefore some of the classes methods should be constant too.

Use Other Libraries Properly

Other libraries, notably the STL, rely on you to write const correct code if you are to use them fully. For example, the equality operator (operator==) is often used to find a particular member of a list template, but since this method should not modify the object, it is reasonable for the list class implementor to expect that it can be applied to a constant list containing constant items. Unless the operator is declared as a constant method, the template expansion will fail. In general, all of the comparison operators (==, !=, <, > etc) should be constant, and copy constructors should take constant parameters.

Const Correct Pointers

Whilst the concepts above may be fairly self-explanatory to seasoned C++ engineers, the issue of const and pointers is often misunderstood. It all boils down to two fundamental questions:

In the first case, your pointer should be declared as follows:

T *const pT;

This this pointer is a good example of this in action. You wouldn't try to make this point to something else would you? In the second case, your pointer should be declared as follows:

const T *pT;

The thing to the right of the const keyword is actually constant.

Of course, the two can be combined to form a constant pointer to a constant as follows:

const T *const pT;

Practical Considerations of Const Correctness

There can be a few pitfalls to acheiving const nirvana, here are some to watch out for.

Other People

In the real world, you have to use API's provided by other people, either in-house engineers or third parties and they may not write const correct code. If they are in-house engineers you may be able to persuade them to change their code but this is unlikely if the code is from a third party.

So what do you do if your code is perfectly const correct but you must call a non-constant method from one of your constant ones as illustrated by the following example?

class Cbad_class {
private:
    int m_iMyValue;
public:
    int GetMyValue();   // A non-constant method, BOO! HISS!
};

class Cgood_class : public Cbad_class {
public:
    int GetValuePlusOne() const {   // Const correct HOORAY!
        return GetMyValue() + 1;   // Oh dear a compiler error
    }
};

In the example above, the derived class (Cgood_class) must inherit from Cbad_class which is not const correct. This causes a compiler error in the GetValuePlusOne() method since it calls a non-const method of the base class. Do not simply remove the const keyword, help is at hand!

A better (but much uglier) solution is to cast the this pointer to be non-const as fllows:

return const_cast<CgoodClass*const>(this)->GetMyValue()+1;

This is hideously ugly code, but it has two advantages - it shows that you have thought about what you are doing and it means that engineers who must extend your class do not have to put up with the same problems.

Mutable Objects

The last release of the C++ standard introduced a new keyword - mutable - to deal with a particular const correctness problem. If a constant function really must change data then that data can be declared a mutable.

For example, if you are implementing a class which must reference count a resource, making constant copies of that class must also increment the reference count. A smart pointer is a good example of this.

Constant Data Members

Where a class contains a member variable that is constant, the means of initialising this variable is not immediately apparent. It should be given a variable in the constructor but by the time the body of the constructor is reached, it has already been assigned a default value. For exmaple:

class Cmy_class {
private:
    const int m_iMyValue;
public:
    Cmy_Class() {   // A constructor
        m_iMyValue = 2;   // Compiler error
    }
};

The solution is to use the constructor's initialiser list to override the default value assigned by the compiler. The code should look as follows:

Cmy_Class() : m_iMyValue(2) {}   

Notice the empty space between the {}'s.

Collection Classes

When you're using a collection class like MFC's CArray or the STL's vector classes, you may want a collection of constant objects but a compile error occurs when you try to add an item to the collection. This is becuase the collection class allocates a series of objects at a time using the default constructor in order to save time when growing the collection. It then uses the assigment operator to copy into one of the blank objects from the one you're trying to add.

This is essentially a trade-off between efficiency and correct code.

Thanks go to Martin for pointing this out.

Common Examples of Const Correctness

Here's a bunch of class member functions which I think should always use const, after reading this article, I hope you can work out why:

Assignment Operator

const T &operator=(const T &);

Comparison Operators

bool operator==(const T &) const;
bool operator!=(const T &) const;
bool operator<=(const T &) const;
bool operator>=(const T &) const;
bool operator<(const T &) const;
bool operator>(const T &) const;
ostream &operator<<(ostream &) const;

Finally, if you provide any of the following member functions, you should consider providing a const equivalent:

Any type cast operator, e.g. operator int(); might also provide operator const int() const;
T *operator->(); should also have const T *operator->() const;
T &operator*();
should also have const T &operator*() const;
T &operator[](size_t);
should also have const T &operator[](size_t) const;

Comments

Members have left 0 comments about this page:
Please Login or Register to comment on this page.

Resources
Tools
User
Last Updated Wednesday, 24-May-2006 22:39:16 BST