Const Correctness Part 1

by Darren Collins
Sunday, 21 March 2004

An alert reader, Frank Castellucci, responded to last week's article on iterators by pointing out that my example functions weren't particularly efficient, and would get worse as the vector v increased in size.

void printVec(vector<int> v)

The reason is that the above function actually causes a copy of the vector v to be created, and it's this copy of v that gets manipulated inside the function. If I passed v by reference instead of by value, the copy would be avoided.

void printVec(vector<int> &v)

That fixes efficiency concerns, but it's still not perfect. As Frank rightly pointed out, the function also needs to be const-correct.

What's that?

Const-correctness is a nice, although often-ignored, feature of C++. You can tell the caller (and the compiler), through the function prototype, that a given parameter to a function won't be modified within that function. There are several benefits to this:

  • Correctness: If you accidentally modify a variable that you shouldn't, you'll catch the error at compile time rather than waiting for it to bite you at run time.
  • Reliability: Callers are guaranteed that your function won't modify any variables they pass in as const parameters.
  • Documentation: By looking at your interface, callers know which parameters are modified and which aren't. They don't need to read through your code to figure it out. This also helps future maintainers of your code.
  • Efficiency: Potentially, the compiler can apply optimisations to your code if it knows which parameters remain constant throughout a function.

There are three ways to pass parameters to a function, illustrated below:

void f1(int i);     // pass by value
void f2(int &i); // pass by reference
void f3(int *i); // pass by pointer
  • f1 copies i for use inside the function. It can modify the copy of i, but when it returns to the caller the original variable i remains unchanged.
  • f2 passes a reference to i into the function, which allows i to be modified.
  • f3 passes a pointer to i into the function, which allows i to be modified by dereferencing the pointer.

Note also that f1 causes a copy of i to be created (using i's copy constructor), but f2 and f3 don't. For all but the most basic parameter types, this makes f2 and f3 more efficient than f1.

Getting back to const-correctness, if you don't intend to modify the parameter inside the function, you should declare it as constant. The three functions below show the const-correct forms corresponding to the examples above:

void g1(const int i);
void g2(const int &i);
void g3(const int *i);
  • g1 has the same effect as f1 as far as the user is concerned. It also imposes the extra restriction that the copy of i cannot be modified inside the function. This only affects the function's implementation, and is transparent to the caller.
  • g2 passes a reference to i into the function, but that reference can't be modified.
  • g3 passes a pointer to i into the function, but that pointer can't be modified even using dereferencing.

Again, g1 causes a copy of i to be created but g2 and g3 don't, making them (generally) more efficient than g1. In addition, g2 and g3 won't let i be modified inside them - you'll get a compiler error if you try.

To help demonstrate the above concepts, I've created my own simple class that has a modifiable member variable and prints messages in its constructors and destructor. This will let us see when copies are created in function calls.

Demo Program

#include <iostream>
using namespace std;
class NoisyInt
    NoisyInt(const NoisyInt& i) : _num(i.get_num())
      { cout << "Copy-constructing " << _num << " ["
             << this << "]" << endl; }
    NoisyInt(const int i) : _num(i)
      { cout << "Constructing " << _num << " ["
             << this << "]" << endl; }
      { cout << "Destructing " << _num << " ["
             << this << "]" << endl; }
    void increment() { _num++; }
    const int get_num() const { return _num; }
    int _num;

void f1(NoisyInt i)
  cout << "Called f1() - incrementing i" << endl;
  cout << "New i = " << i.get_num() << endl;

void g1(const NoisyInt i)
  cout << "Called g1() - can't increment i" << endl;
  // i.increment();

void f2(NoisyInt &i)
  cout << "Called f2() - incrementing i" << endl;
  cout << "New i = " << i.get_num() << endl;

void g2(const NoisyInt &i)
  cout << "Called g2() - can't increment i" << endl;
  // i.increment();

void f3(NoisyInt *i)
  cout << "Called f3() - incrementing i" << endl;
  cout << "New i = " << (*i).get_num() << endl;

void g3(const NoisyInt *i)
  cout << "Called g3() - can't increment i" << endl;
  // (*i).increment();

int main()
  cout << "Start of program." << endl;
  NoisyInt Number(15);
  cout << "End of program." << endl;

Demo Program Output

Start of program.
Constructing 15 [0xbffffbc4]
Copy-constructing 15 [0xbffffbc0]
Called f1() - incrementing i
New i = 16
Destructing 16 [0xbffffbc0]
Copy-constructing 15 [0xbffffbc0]
Called g1() - can't increment i
Destructing 15 [0xbffffbc0]
Called f2() - incrementing i
New i = 16
Called g2() - can't increment i
Called f3() - incrementing i
New i = 17
Called g3() - can't increment i
End of program.
Destructing 17 [0xbffffbc4]

By following the output of the program, you can see that f1 and g1 both create a copy of their parameter (the memory addresses of both the original and the created objects are printed to show they are different). f1 is legally allowed to modify the copy of the parameter, although the original variable (with a value of 15) remains unchanged. Uncomment the line in g1 to see what error your compiler generates when you try to modify a const parameter.

f2, f3, g2 and g3 don't create a copy of the parameter. f2 and f3 can modify the parameter, and the change directly affects the original variable. Again, g2 and g3 can't legally modify the parameter (try removing the comments in those functions to test this).


Whenever you create functions in C++, you should bear in mind const-correctness. As a general rule of thumb, any parameter that won't be modified in a function should be passed in as a constant reference (as in g2 above). The benefits of this method are:

  • The function call looks identical to f1 from the caller's perspective (g3 requires dereferencing which often leads to errors and confusion).
  • The creation of a copy of the parameter is avoided, decreasing function call overhead (g1 causes a copy to be created).
  • Although it hasn't been covered in this article, g2 allows a temporary object to be passed in, but g3 does not (C++ doesn't allow you to create a pointer to a temporary object).

Back to the original problem with my code from last week - my function should have looked like this:

  void printVec (const vector<int> &v)
    vector<int>::const_iterator pos = v.begin();

The const_iterator is just like an ordinary iterator, except it operates on a const container. Likewise, the revPrintVec function should be modified to pass (const vector<int> &v) and use a const_reverse_iterator. After making these modifications, the program will work just as before (only a bit more efficiently!).

When you're writing software from scratch, it's not hard to make sure your functions and objects obey const-correctness. This little extra effort will set you up with more reliable, better documented and more maintainable code that will scale well as your program grows. It's also likely to be more efficient than the equivalent code without const-correctness, since the compiler has more optimising options.

Retro-fitting const-correctness to existing code is a frustrating uphill battle, as changes to function parameters propagate throughout your project. It's far better to do it right in the first place and avoid the problem.


Related Articles
- Links - C/C++
- Code Layout Styles
- Const Correctness Part 1
- Const Correctness Part 2
- Const Correctness Part 3
- Const Correctness Part 4
- Const Correctness Part 5
- Const Correctness Part 6

This site Copyright 1999-2005 Darren Collins.