C++: Constants

It's not uncommon to want some values to remain untouched and unchanged from certain processes. One of the easiest ways of doing this is to use the const qualifier. A qualifier, in C++, is simply something that modifies, or adds a quality to, what follows. These behave much like an adjective in a sentence: "the weary fellow", "the old hag", "the constant integer".

The easiest way to make use of the const modifier is in a simple variable declaration. Take, for example, the following snippet of code which does not utilize this modifier:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
#include <iostream>

using namespace std;

int main()
{
	float PI = 3.14159;
	int r;

	cout << "Radius: ";
	cin >> r;

	cout << "Area: "<< PI * r * r;

	return 0;
}

The above code snippet should work fine, but let's pretend that the main snippet of code is actually part of a much bigger program. As such, we made a little typo before calculating and outputting the area in which we meant to set "PINE", some other variable in our pretend big program, but accidentally set "PI":

1
2
3
4
5
6
7
8
9
10
11
12
13
14
int main()
{
	float PI = 3.14159;
	int r;

	cout << "Radius: ";
	cin >> r;

	PI = 21;

	cout << "Area: "<< PI * r * r;

	return 0;
}

The worst thing about the above code is that although "Area" results will now always be incorrect, we may not have a clue that this is the case! Our compiler doesn't throw an error at us because what we're doing is technically correct, but we've wrecked the value of "PI" which we may be using in a variety of places throughout the rest of the application code. Such errors are difficult to track down (Are we getting the input wrong? Is our formula incorrect?), and as such, we can use the const qualifier on the "PI" variable to make the value a read-only value.

This means that the variable must be initialized as soon as it is declared so that it can possess a value at all, and from there the value cannot be modified -- the compiler will throw you an error if you try. The qualifier, in this case, should go before the data-type. So if we changed "PI" to be a constant (using const), our compiler would luckily throw an error at us when we try to change its value:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
#include <iostream>

using namespace std;

int main()
{
	const float PI = 3.14159;
	int r;

	cout << "Radius: ";
	cin >> r;

	cout << "Area: "<< PI * r * r;

	return 0;
}

The error should tell you that the variable is read-only or that you simply cannot assign to it, and as such you can jump in and fix or remove the line causing the problems. Phew!

One of the other extremely useful uses of the const qualifier is when passing to functions or member functions by reference. Despite the speed and memory advantages to doing so, it can be dangerous as the values we pass in could be directly modified by the function (by accident). Take for example the following where we accidentally change the value of 'value' via the function:

1
2
3
4
5
6
7
8
9
10
11
12
void output(int& v)
{
	v = 1; //An accident! Uh oh!
	cout << v; //We'll see that 'v' isn't 5! Oh no!
}
int main()
{
	int value = 5;
	output(value);

	return 0;
}

Again, the problems in the above may be difficult to isolate in a larger program, and as you may have guessed, const makes for a very good solution here. So instead of our function taking a regular integer reference, it can take a const int& v (constant integer reference)! This means that our function won't actually be able to modify the value that 'v' is aliasing ('value'), however we'll still be able to mess with 'v' in other situations (e.g. just in main). With the const modification, we'll get an error thrown for the attempted modification of a const parameter (Hurrah!):

1
2
3
4
5
6
7
8
9
10
11
12
void output(const int& v)
{
	v = 1; //Error from trying to modify a const parameter
	cout << v;
}
int main()
{
	int value = 5;
	output(value);

	return 0;
}

The above means that you can get the speed and memory advantages of passing by reference, while preventing the danger of modifying variables that you don't want to be changed. You can also set const on 'regular' parameters too, however this often isn't as useful (although can be in some situations).

We should also talk a bit about constant pointers, as they can be a little bit more confusing. This confusion mainly comes from the fact that when dealing with pointers, there are two types of constant which you may wish to achieve. One is not being able to change what the pointer points to, and the other is not being able to change the value of the thing which the pointer points to. The latter can be accomplished easily by putting the const qualifier before the data-type, which would create, for example, a pointer to a constant integer: const int* ptr = &n;. This functionality means that we cannot change *ptr, as shown in the following code snippet:

1
2
3
4
5
int n = 5, o = 10;
const int* ptr = &n; //ptr points to n

*ptr = 5; //Trying to change the value of 'n' - not allowed! Error!
ptr = &o; //Change where ptr points, perfectly allowed in this case.

The other kind of pointer constant can be achieved by using the const qualifier after the data-type and dereference operator, for example int* const ptr:

1
2
3
4
5
int n = 5, o = 10;
int* const ptr = &n; //ptr points to n

*ptr = 5; //Change the value of 'n', perfectly allowed in this case.
ptr = &o; //Trying to change where ptr points - not allowed! Error!

If necessary, the qualifier can actually be used in both places - so a constant integer constant pointer, if you like, would be represented as const int* const ptr_name;.

The final use of const which we're going to talk about in this tutorial is the use of the qualifier on class/struct member functions. To demonstrate this, let's firstly set up a basic class structure with some function prototypes (we're going to do it with a separate structure and implementation, simply for practice):

1
2
3
4
5
6
7
8
class Number
{
public:
	Number() { n = 5; } //Simple constructor with implementation
	int square(); //Prototype for 'square' function which will calculate the square of 'n'

	int n;
};

Before we even get on to the implementation of the 'square' function, we need to add the const qualifier to the prototype too. The qualifier can be added to a member function by very simply typing the const keyword after the parameter brackets (note that putting it before the data-type would have a different effect entirely). With this knowledge of qualifier placement in this context, we can modify our prototype appropriately and then implement the member function by using the scope operator (or if you want to, you could just implement it directly in the 'class' as we've done previously - structuring and prototyping is more common in larger applications where separate header and source files come in to play):

1
2
3
4
5
6
7
8
9
10
11
12
13
class Number
{
public:
	Number() { n = 5; } //Simple constructor with implementation
	int square() const; //Prototype for 'square' function which will calculate the square of 'n'

	int n;
};

int Number::square() const //Implementation for Number::square
{
	return n*n;
}

Adding the qualifier in this context essentially just means that the member function can't modify any of the member variables (in this case, it can't modify 'n'). The other important thing about these constant member functions is that they can only call other constant member functions. If such a member function were to just call another member function to modify the value of a member variable, this would just defeat the whole point of the 'const' in this context, and as such, only other 'const' member functions can be called from these. In this case, it makes sense to build this functionality in as the 'square' member function shouldn't be modifying any member variables, and shouldn't call any other member functions that will (or even have the ability to) do so.