Constants in C++

Introduction


Constants in C++ are specified using the const keyword. const keyword refers to one of the most versatile features of the language which not generally completely understood by a large number of developers. Most use this keyword to either declare global constants in their programs or to specify that objects and structures being referenced inside a method cannot be changed. In addition to these uses, the const keyword can be used to specify whether an object whose reference returned by a method can be changed in the calling method. Also member functions can be specified that do not allow member variables of the object to which these methods belong to be changed.

This article attempts to clarify most of the uses of const with the help of examples. These examples have been coded and tested over Linux.


Defining Constants in a Program


As mentioned above, one of the most common uses of const is to declare constants in a program. Unlike symbolic constants defined using #defines, constants defined using the const keyword reside in a memory location contents of which cannot be changed. Furthermore significant control can be exercised on their scope and visibility, i.e., declaring them within function, passing them as arguments, etc. Finally const allows declaring constant user defined types such as structures and objects.

The following example demonstrates the simplest use of const, i.e., declaring constants in a program.

#include <iostream>
#include <math.h>

using namespace std;

int main(void)
{
   const float pi = 3.141;
   float area;
   float circumference;
   float radius;
   
   cout << "Enter radius: ";
   cin >> radius;
   
   area = pi * pow(radius, 2.0);
   circumference = 2 * pi * radius;
   
   cout << "Area          = " << area << endl;
   cout << "Circumference = " << circumference << endl;
   
   return (0);
}

The following program demonstrate the passing of constants declared in function main as arguments to other functions.

#include <iostream>
#include <math.h>

using namespace std;

float getArea(const float radius, const float pi)
{
   float area;
   area = pi * pow(radius, 2.0);
   return area;
}

float getCircumference(const float radius, const float pi)
{
   float circumference;
   circumference = 2 * pi * radius;
   return circumference;
}

int main(void)
{
   const float pi = 3.141;
   float radius;
   
   cout << "Enter radius: ";
   cin >> radius;
   
   
   cout << "Area          = " << getArea(radius, pi) << endl;
   cout << "Circumference = " << getCircumference(radius, pi) << endl;
   
   return (0);
}

As the arguments are passed to the functions getArea and getCircumference with the const keyword, it is not possible to re-assign them other values within these functions. Therefore, the following program, when compiled, will give an error.

#include <iostream>
#include <math.h>

using namespace std;

float getArea(const float radius, const float pi)
{
   float area;
   area = pi * pow(radius, 2.0);
   return area;
}

float getCircumference(const float radius, const float pi)
{
   float circumference;
   radius = 5.0;     // Will give a compilation error.
   circumference = 2 * pi * radius;
   return circumference;
}

int main(void)
{
   const float pi = 3.141;
   float radius;
   
   cout << "Enter radius: ";
   cin >> radius;
   
   
   cout << "Area          = " << getArea(radius, pi) << endl;
   cout << "Circumference = " << getCircumference(radius, pi) << endl;
   
   return (0);
}

The following program demonstrate the declaration and initialisation of constants of user defined types. A constant of structure point is declared and initialised along with a variable of the same type.

#include <iostream>

using namespace std;

typedef struct
{
   int x;
   int y;
} point;

int main(void)
{
   const point fixedLocation = {5,3};
   point variableLocation;
   
   variableLocation.x = 6;
   variableLocation.y = 7;
      
   cout << "Variable location is " << variableLocation.x << ":" << variableLocation.y << endl;
   cout << "Fixed location is    " << fixedLocation.x << ":" << fixedLocation.y << endl;
   
   variableLocation.x = 4;
   variableLocation.y = 9;
      
   cout << "Variable location is " << variableLocation.x << ":" << variableLocation.y << endl;
   cout << "Fixed location is    " << fixedLocation.x << ":" << fixedLocation.y << endl;

   return (0);
}

The output of this program is given below,

Variable location is 6:7
Fixed location is    5:3
Variable location is 4:9
Fixed location is    5:3
While the values of members of the variable can be changes, changing the values of the members of the constant (as shown below) will give an error on compilation.

#include <iostream>

using namespace std;

typedef struct
{
   int x;
   int y;
} point;

int main(void)
{
   const point fixedLocation = {5,3};
   point variableLocation;
   
   variableLocation.x = 6;
   variableLocation.y = 7;
      
   cout << "Variable location is " << variableLocation.x << ":" << variableLocation.y << endl;
   cout << "Fixed location is    " << fixedLocation.x << ":" << fixedLocation.y << endl;
   
   variableLocation.x = 4;
   variableLocation.y = 9;
   
   fixedLocation.x = 6; // This will give a compilation error.
      
   cout << "Variable location is " << variableLocation.x << ":" << variableLocation.y << endl;
   cout << "Fixed location is    " << fixedLocation.x << ":" << fixedLocation.y << endl;

   return (0);
}

const with Pointers

For pointers, const can be used to specify whether the pointer (i.e., the address) itself is a constant or the data pointed to by the pointer is a constant. Consider the following examples.

const char* p;      // The data pointed to by the pointer p is constant.
                    // The pointer itself is variable.
char* const p;      // The pointer itself is constant but the data to 
                    // which points is not constant.
const char* const p;// Both the pointer and the data pointed to by the 
                    // pointer are constant.

As a general rule, if the keyword const is to the left of *, the data being pointed to is constant. If the keyword const is to the right of *, then the pointer is constant. Consider the following program. Here, the pointer locPtr is used to point to structure location1 and then later in the program to structure location2 (variation in the value of locPtr). locPtr is also used to refer to the element x of location2 and change its value (variation in the data pointed to by locPtr).

#include <iostream>

using namespace std;

typedef struct
{
   int x;
   int y;
} point;

int main(void)
{
   point location1 = {6,3};
   point location2 = {4,-8};

   point* locPtr; 

   // Assigning a pointer to locPtr
   locPtr = &location1;
   cout << locPtr->x << ":" << locPtr->y << endl;

   // Changing the locPtr assignment
   locPtr = &location2;
   cout << locPtr->x << ":" << locPtr->y << endl;

   // Changing the contents of the structure pointed to by locPtr
   locPtr->x = -4;
   cout << locPtr->x << ":" << locPtr->y << endl;
   return 0;
}

The output of this program is shown below,

6:3
4:-8
-4:-8

However, in the following program, because locPtr is declared as const point* locPtr;, the compiler gives an error on line 27 as read-only access to location2 via locPtr is allowed.

#include <iostream>

using namespace std;

typedef struct
{
   int x;
   int y;
} point;

int main(void)
{
   point location1 = {6,3};
   point location2 = {4,-8};

   const point* locPtr; 

   // Assigning a pointer to locPtr
   locPtr = &location1;
   cout << locPtr->x << ":" << locPtr->y << endl;

   // Changing the locPtr assignment
   locPtr = &location2;
   cout << locPtr->x << ":" << locPtr->y << endl;

   // Changing the contents of the structure pointed to by locPtr
   locPtr->x = -4; // This will give an error on compilation.
   cout << locPtr->x << ":" << locPtr->y << endl;
   return 0;
}

In the following program, because locPtr is declared as point* const locPtr;, the compiler gives an error on line 21 as locPtr itself is read-only.

#include <iostream>

using namespace std;

typedef struct
{
   int x;
   int y;
} point;

int main(void)
{
   point location1 = {6,3};
   point location2 = {4,-8};

   point* const locPtr = &location1; 

   cout << locPtr->x << ":" << locPtr->y << endl;

   // Changing the locPtr assignment
   locPtr = &location2; // This will give an error on compilation.
   cout << locPtr->x << ":" << locPtr->y << endl;

   // Changing the contents of the structure pointed to by locPtr
   locPtr->x = -4; 
   cout << locPtr->x << ":" << locPtr->y << endl;
   return 0;
}

Finally, in the following program, because locPtr is declared as const point* const locPtr;, compilation errors are generated for both lines 21 and 25.

#include <iostream>

using namespace std;

typedef struct
{
   int x;
   int y;
} point;

int main(void)
{
   point location1 = {6,3};
   point location2 = {4,-8};

   const point* const locPtr = &location1; 

   cout << locPtr->x << ":" << locPtr->y << endl;

   // Changing the locPtr assignment
   locPtr = &location2; // This will give an error on compilation.
   cout << locPtr->x << ":" << locPtr->y << endl;

   // Changing the contents of the structure pointed to by locPtr
   locPtr->x = -4; // This will give an error on compilation.
   cout << locPtr->x << ":" << locPtr->y << endl;
   return 0;
}

Passing Constant References as Arguments


In addition to passing structures and objects to functions via pointers, these can be passed as references. If the structure or object is not to be mutated within the function, the declaration of the formal argument is preceded by the const keyword signifying that the values of members of the object referenced to should not be change.

Consider the following example, the setPerson method takes in a reference to a variable of structure Person and assigns its members values passed in via the command line. The function printPerson takes in a reference to the same structure. However, as the structure is not to be mutated within this function, the const keyword precedes the declaration of the formal argument aPerson.


#include <iostream>
#include <string>
#include <cstdlib>
using namespace std;

typedef struct
{
	int year;
	int month;
	int day;
} Date;

typedef struct
{
	string name;
	Date dateOfBirth;
} Person;

void printPerson(const Person& aPerson)
{
	cout << aPerson.name << endl;
	cout << aPerson.dateOfBirth.day << "-";
	cout << aPerson.dateOfBirth.month << "-";
	cout << aPerson.dateOfBirth.year << endl;
}

void setPerson(Person& aPerson, char* name,
	       char* year, char* month, char* day)
{
	aPerson.name = name;
	aPerson.dateOfBirth.year = atoi(year);
	aPerson.dateOfBirth.month = atoi(month);
	aPerson.dateOfBirth.day = atoi(day);
}

int main(int argc, char* argv[])
{
	Person aPerson;
	if (argc < 5)
	{
		cout << "USAGE::const_ref_simple <name> <year> <month> <day>" << endl;
	}
	else
	{
		setPerson(aPerson, argv[1], argv[2], argv[3], argv[4]);
		printPerson(aPerson);
	}
	return (0);
}

Compiling the program as cons_ref_simple and executing as below results in the output shown below,

[root@big-hug notes]# ./cons_ref_simple Omar 2000 10 12
Omar
12-10-2000

However, declaring the function setPerson as follows

void setPerson(const Person& aPerson, char* name,
	       char* year, char* month, char* day)

will result in the following compilation error, signifying that changes to the values of members of a structure referenced using a const reference are not allowed.

const_ref_simple2.cpp: In function `void setPerson(const Person&, char*, char*, 
   char*, char*)':
const_ref_simple2.cpp:30: passing `const std::string' as `this' argument of `
   std::basic_string<_CharT, _Traits, _Alloc>& std::basic_string<_CharT, 
   _Traits, _Alloc>::operator=(const _CharT*) [with _CharT = char, _Traits = 
   std::char_traits<char>, _Alloc = std::allocator<char>]' discards qualifiers
const_ref_simple2.cpp:31: assignment of data-member `Date::year' in read-only 
   structure
const_ref_simple2.cpp:32: assignment of data-member `Date::month' in read-only 
   structure
const_ref_simple2.cpp:33: assignment of data-member `Date::day' in read-only 
   structure

Reference to mutable objects should be passed as const references to functions where states of these objects are not be changed. Methods of classes which can be invoked on const objects and const object references are to be specified as const as well. Not only do they make the interface of a class more intuitive but also allow operations on const objects and const object references. A method is defined as const by the keyword const following the method declaration.

The following source code explains the use of const methods on const objects.

#include <string>
#include <iostream>

using namespace std;

class PhoneNumber
{
	private:
		int areaCode;
		int number;
		
	public:
		PhoneNumber(int ac, int num):areaCode(ac),number(num){}
		
		int getAreaCode(void) const
		{
			return areaCode;
		}
		
		int getNumber(void) const
		{
			return number;
		}
		
		void setNumber(int ac, int num)
		{
			areaCode = ac;
			number = num;
		}
};

class Person
{
	private:
		string firstName;
		string lastName;
		mutable PhoneNumber phoneNumber;
		
	public:
		Person(const string& fname, const string& lname,
		       int areaCode, int number):firstName(fname), 
				                 lastName(lname),
				                 phoneNumber(areaCode, number){}
								  
		const string& getFirstName(void) const
		{
			return firstName;
		}
		
		const string& getLastName(void) const
		{
			return lastName;
		}
		
		const PhoneNumber& getPhoneNumber(void) const
		{
			return phoneNumber;
		}
		
		void setPhoneNumber(const PhoneNumber& inNumber) const
		{
			phoneNumber = inNumber;
		}
};

void printPerson(const Person& aPerson)
{
	cout << aPerson.getFirstName() << ":";
	cout << aPerson.getLastName() << ":";
	cout << aPerson.getPhoneNumber().getAreaCode() << "-";
	cout << aPerson.getPhoneNumber().getNumber() << endl;
}

void setPhoneNumber(const Person& aPerson, 
                    const PhoneNumber& phoneNumber)
{
	aPerson.setPhoneNumber(phoneNumber);
}

int main(void)
{
	Person aPerson("Omar", "Bashir", 1234,567890);
	printPerson(aPerson);
	PhoneNumber phoneNumber(4321,987650);
	setPhoneNumber(aPerson,phoneNumber);
	printPerson(aPerson);	
	return (0);
}

The phoneNumber member of the Person class has been declared as mutable. A mutable member can be changed within a const method. In this example, a person's identity cannot be changed however the phone number can always change. Therefore it has been specified as mutable and can be changed via the setPhoneNumber const method. If the mutable keyword is removed, compiling the above mentioned program produces the following errors.

[root@big-hug notes]# g++ -Wall const_ref2.cpp -o const_ref2
const_ref2.cpp: In member function `void Person::setPhoneNumber(const PhoneNumber&) const':
const_ref2.cpp:62: passing `const PhoneNumber' as `this' argument of ` PhoneNumber&
PhoneNumber::operator=(const PhoneNumber&)' discards qualifiers

Returning Constant References from Functions

In the above example, the accessor methods of the Person class return const references to the respective data members. Returning const references are useful if the objects to be returned are mutable and returning a value may result in a significant copy construction operation. However, references to local variables and variables declared on the heap should not be returned as the former will reference to stale memory in the stack and the latter may be deleted by the owner object again resulting in references to stale memory or may result in memory leaks if such an object is created only to return the reference.

Consider the statement shown below inserted in the main function of the above mentioned program,

aPerson.getPhoneNumber().setNumber(2468,135791);

This will result in the following compilation error,

[root@big-hug notes]# g++ -Wall const_ref2.cpp -o const_ref2
const_ref2.cpp: In function `int main()':
const_ref2.cpp:84: passing `const PhoneNumber' as `this' argument of `void 
   PhoneNumber::setNumber(int, int)' discards qualifiers