C++ - Const Keyword

The const keyword is used to define more than just constants...

Variables

The use of const keyword to define constants removes the need to use the #define preprocessor command. The use of const ensures that the symbol is entered into the symbol table and if there are errors related to the user of the constant the compiler can give a better diagnosis.

const int a = 1999; // constant integer
// a = 2000; // not allowed

const string b("the year 1999"); // constant string
// b = "was a good one"; // not allowed

const string * c = new string ("variable pointer to a constant string");
// c[0] = 'k'; // not allowed
c = new string("variable pointer can point to another string");
// const T * is equivalent to T const *

string * const d = new string("constant pointer to a variable string");
d[0] = 'k';
// d = new string("constant pointer cannot point to another location"); // not allowed

const string * const e = new string("constant pointer to a constant string");
// e[0] = 'k'; // not allowed
// e = new string("won't work'); // not allowed

The scope of a constant can be limited to within a class, when its declared as -
const string b("the year 1999");
When combined with the static keyword, there is only one copy of the constant -
static const string b("the year 1999);
The #define command does not allow defining a class level constant.

Functions

Return type of a function can be const -

const char * WhichYearWasGood()
{
  return "1999";
}

// char * year = WhichYearWasGood(); // not allowed
// year[0] = 2000;

Arguments to a function can be const. Usually const pointers are passed to functions if the cost of copy (in pass by value) is a concern and/or if arguments should not be modified -

int GetStringLength(const char * const s)
{
  // s[0] = 'k'; // not allowed
  // s = new string("won't work'); // not allowed
  return strlen(s);
}

Member functions which do not/should not modify any member data are declared const as -

class String
{
  private:
    int length;
    char * string;
  public:
    int GetLength() const
    { 
      // ++length; // not allowed
      return length;
    }
};

The class basic_string overloads the operator[] to return const char& or char& depending on whether the member function was invoked on a const object or non-const object -

Notice that the second function -
reference operator[](size_type __pos)
returns a reference. So that the caller of the function can use the return value to modify the character at position __pos. The function could have been declared as -
reference operator[](size_type __pos) const
which would have been syntactically correct as the function does not modify any data member internally, however since its return value can be used to modify the underlying string it would be semantically incorrect to use this declaration.

STL

An iterator is similar to a pointer. A const iterator is equivalent to a constant pointer.



Custom Arithmetic Operators

When overloading arithmetic operators like +, -, *, /, etc its best to define the return type as const.

class T {
  public :
    const T operator + (const T& lhs, const T& rhs);
    const T operator - (const T& lhs, const T& rhs);
    const T operator * (const T& lhs, const T& rhs);
    const T operator / (const T& lhs, const T& rhs);
};

This prevents users of class T from writing the following code -

T lhs, rhs, weird;
(lhs + rhs) = weird;
(lhs - rhs) = weird;
(lhs * rhs) = weird;
(lhs / rhs) = weird;

Mutable

A class member function declared const implies that it does not modify any internal data members. This declaration is for the users of the class, from their perspective the member function 'does not appear' to modify anything. However, in some scenario's it becomes necessary to modify without affecting the users assumptions. For example, a string class could implement -
size_t length() const
{ return _length; }
The class could be optimized to not compute length till it is required. The first such requirement may be when this function is invoked, at this point the function may first compute length and then return it -
size_t length() const
{
  _length = computeLength();
  return _length;
}
This is possible if the _length variable is declared as mutable -
class string {
  private :
    mutable size_t _length;
    size_t computeLength();
  public :
     size_t length() const;
};

Casting

const_cast<T> allows casting away constness on an object of type T -
const string * x = new string("hello");
// *x = "bye"; // not allowed
string * y = const_cast<string*>(x);
*y = "bye"; // ok, no longer a const string
cout<<x->c_str(); // prints 'bye'
This is all the const_cast does i.e. remove constness.

static_cast<T> allows casting constness on to an object of type T, and is also useful for other casts.
const string * x = new string("hello");
// *x = "bye"; // not allowed
string * y = const_cast<string*>(x);
*y = "bye"; // ok, no longer a const string
cout<<x->c_str(); // prints 'bye'!v
const string * z = static_cast<const string *>(y);
cout<<"z = "<<z->c_str()<<endl;

Make const functions thread safe


If a const function operates on a single variable then using std::atomic is sufficient. Example -
class ABC {
  private:
    int x;
    mutable int count;
  public:
    int getX() const;
    {
       ++count; //could be a problem if getX() is invoked from two parallel threads
        return x;
     }
};

Here getX() keeps a count of the number of times it's called. Declaring it as std::atomic<int> would be sufficient.

When more than one variable is involved, a mutex/lock mechanism needs to be employed. Example -
class ABC {
  private:
    int x;
    int y;
    mutable std::mutex m;
    mutable bool isProductValid { false };
    mutable int product;
  public:
    int getProduct() const
    {
       std::lock_guard<std::mutex> lg(m); //lock mutex
       if(isProductValid)
         return product;

       product = x * y;
       isProductValid = true;
       return product;
    }
};

This ensures that when the value of isProductValid is read, the product is computed and the value of isProductValid is set there are no race conditions when getProduct() is invoked from multiple threads.