const, volatile, mutable, and More

const, volatile, mutable, and More

In C++, const, volatile, and mutable are type qualifiers that modify the behavior of variables.

const

const specifies that a variable’s value cannot be changed after initialization. Any attempt to modify a const variable will result in a compile-time error.

volatile

volatile indicates that a variable’s value can be changed by external factors outside the control of the program, such as hardware or another thread. It prevents the compiler from performing optimizations that might assume the variable’s value remains constant.

mutable

mutable applies only to class members and allows modification of a member variable even within a const object. It is useful when a class has internal state that needs to be updated without affecting the object’s externally visible const-ness.

mutable may not be used in combination with static or const

Some C++ Specification Jargon

Here is some jargon from the C++ specification which may be a little esoteric, but you may find this jargon used in some compiler error and warning messages.

cv (const and volatile) type qualifiers

Any (possibly incomplete) type other than function type or reference type is a type in a group of the following four distinct but related types:

  • A cv-unqualified version.
  • A const-qualified version.
  • A volatile-qualified version.
  • A const-volatile-qualified version.

Array types are considered to have the same cv-qualification as their element types.

A const volatile object is:

  • an object whose type is const-volatile-qualified,
  • a non-mutable subobject of a const volatile object,
  • a const subobject of a volatile object, or
  • a non-mutable volatile subobject of a const object.

Behaves as both a const object and as a volatile object.

Examples

The following program exhibits a number of compile-time errors:

//
//  main.cpp
//  const, volatile, mutable
//
//  Created by Bryan Higgs on 2/10/25.
//

#include <iostream>

class Example 
{
public:
  Example(int a, int b, int c)
    : constVar(a), nonConstVar(b), volatileVar(c), mutableVar(0)
  {}

  void modify() 
  {
    nonConstVar = 10; // OK
    constVar = 20; // Cannot assign to non-static data member 'constVar' with const-qualified type 'const int'
                   // Non-static data member 'constVar' declared const
    volatileVar = 30; // OK
    mutableVar = 40; // OK
  }

  void constModify() const
  {
    nonConstVar = 10;
    constVar = 20; // Cannot assign to non-static data member within const member function 'constModify'
                   // Member function 'Example::constModify' is declared const
    volatileVar = 30; // Cannot assign to non-static data member within const member function 'constModify'
                      // Member function 'Example::constModify' is declared const
    mutableVar = 40;  // OK to modify a mutable member in a const member function
  }
  
private:
  int nonConstVar;
  const int constVar;
  volatile int volatileVar;
  mutable int mutableVar;
};

int main(int argc, const char * argv[])
{
  const Example constObj(1, 2, 3);
  constObj.constModify();

  Example obj(4, 5, 6);
  obj.modify();

  volatile int vol = 7;
  const volatile int constVol = 8;

  vol = 9; // OK
  constVol = 10; // Cannot assign to variable 'constVol' with const-qualified type 'const volatile int'
                 // Variable 'constVol' declared const
  int x = vol; // Compiler must read the current value of vol
  int y = constVol; // Compiler must read the current value of constVol

  return 0;
}

constexpr Specifier (since C++11)

The idea of constexpr is to provide a performance improvement for programs by doing computations at compile time rather than run time. Note that once a program is compiled and finalized by the developer, it is run multiple times by users. The idea is to spend time in compilation and save time at run time (similar to template metaprogramming – see later…).  

constexpr specifies that the value of an object or a function can be evaluated at compile-time and the expression can be used in other constant expressions. Such entities can then be used where only compile time constant expressions are allowed (provided that appropriate function arguments are given).

A constexpr specifier used in an object declaration or non-static member function (until C++14) implies const.

A constexpr specifier used in the first declaration of a function or static data member (since C++17) implies inline. If any declaration of a function or function template has a constexpr specifier, then every declaration must contain that specifier.

It defines an expression that can be evaluated at compile time.

Constant expressions can be used as non-type template arguments, array sizes, and in other contexts that require constant expressions, for example:

int n = 1;
std::array<int, n> a1; // Non-type template argument is not a constant expression
                       // Read of non-const variable 'n' is not allowed in a constant expression
const int cn = 2;
std::array<int, cn> a2; // OK: “cn” is a constant expression

NOTE: We talk about templates in detail later, so, until then, bear with us…

constexpr variable

A variable or variable template (since C++14) can be declared constexpr if all following conditions are satisfied:

  • The declaration is a definition.
  • It is of a literal type.
  • It is initialized (by the declaration).

constexpr Example

Here is a simple example of the use of constexpr usage:

//
//  main.cpp
//  const, volatile, mutable
//
//  Created by Bryan Higgs on 2/10/25.
//

#include <iostream>

constexpr int size = 5;
int arr[size]; // Valid, size is known at compile time

constexpr int square(int x)
{
  return x * x;
}

int main(int argc, const char * argv[])
{
  constexpr int result = square(size); // evaluated at compile time
  std::cout << "The square of " << size << " is " << result << std::endl;

  int num = 10;
  int runtime_result = square(num); // evaluated at runtime
  std::cout << "The square of " << num << " is " << runtime_result << std::endl;
  
  return 0;
}

The program outputs:

The square of 5 is 25
The square of 10 is 100
Program ended with exit code: 0

constexpr Example with a Class

Here’s an example of constexpr used with a class:

//
//  main.cpp
//  const, volatile, mutable
//
//  Created by Bryan Higgs on 2/10/25.
//

#include <iostream>

// 3. constexpr with class
class Rectangle {
public:
    constexpr Rectangle(int w, int h) : width(w), height(h) {}
    constexpr int area() const { return width * height; }
private:
    int width;
    int height;
};

int main(int argc, const char * argv[])
{
    constexpr Rectangle rect(5, 10);
    constexpr int rect_area = rect.area(); // evaluated at compile time
    std::cout << "The area of the rectangle is: " << rect_area << std::endl;
    
    return 0;
}

This program outputs:

The area of the rectangle is: 50
Program ended with exit code: 0

constexpr Example to Calculate the Size of an Array at Compile Time

//
//  main.cpp
//  const, volatile, mutable
//
//  Created by Bryan Higgs on 2/10/25.
//

#include <iostream>

constexpr int product(int x, int y) { return (x * y); }

int main(int argc, const char * argv[])
{
  int arr[product(2, 3)] = {1, 2, 3, 4, 5, 6};
  std::cout << "Size of arr: = " 
            << sizeof(arr)/sizeof(arr[0]) << std::endl;
  
  return 0;
}

This program outputs:

Size of arr: = 6
Program ended with exit code: 0

constexpr Example to Convert from One Unit to Another

//
//  main.cpp
//  const, volatile, mutable
//
//  Created by Bryan Higgs on 2/10/25.
//

#include <iostream>
#include <cmath> // For standard declaration of pi (M_PI)

constexpr double ConvertDegreeToRadian(const double& degrees)
{
  return (degrees * (M_PI / 180));
}

int main(int argc, const char * argv[])
{
  double angleInRadians = ConvertDegreeToRadian(90.0);
  std::cout << "Angle in radians: " << angleInRadians << std::endl;
    
  return 0;
}

which outputs:

Angle in radians: 1.5708
Program ended with exit code: 0

constexpr Example to Calculate a Value in the Fibonacci Sequence

The Fibonacci Sequence

In mathematics, the Fibonacci sequence is a sequence in which each element is the sum of the two elements that precede it. Numbers that are part of the Fibonacci sequence are known as Fibonacci numbers, commonly denoted Fn .

The Fibonacci numbers may be defined by the recurrence relation:

{\displaystyle F_{0}=0,\quad F_{1}=1,}

and

{\displaystyle F_{n}=F_{n-1}+F_{n-2}}

for n > 1.

//
//  main.cpp
//  const, volatile, mutable
//
//  Created by Bryan Higgs on 2/10/25.
//

#include <iostream>

constexpr int fibonacci(int n) 
{
  return (n <= 1) ? n : fibonacci(n-1) + fibonacci(n-2);
}

int main(int argc, const char * argv[])
{
  constexpr int result = fibonacci(10);
  std::cout << "Fibonacci(10) = " << result << std::endl;

  return 0;
}

which outputs:

Fibonacci(10) = 55
Program ended with exit code: 0

NOTE: There is a limit (specific to each C++ compiler) to the evaluation of constexpr compile-time expressions. In the above case, the expression at line 17 is a call to a recursive function. It succeeds for a value of 10 (in my compiler), but if I change the value to 30, I get the following:

Constexpr variable ‘result’ must be initialized by a constant expression
Constexpr evaluation hit maximum step limit; possible infinite loop?

Some compilers may crash instead of emitting such a compile-time error.

consteval and constinit Specifiers (since C++20)

consteval Specifier

The consteval specifier declares a function that must be evaluated at compile time. Any call to a consteval function must occur in a context where a constant expression is required.

Example:

//
//  main.cpp
//  const, volatile, mutable
//
//  Created by Bryan Higgs on 2/10/25.
//

consteval int multiply(int x, int y) 
{
  return x * y;
}

int main(int argc, const char * argv[])
{
  constexpr int result1 = multiply(5, 3); // OK: evaluated at compile time
  static_assert(result1 == 15);

  int a = 5;
  int result2 = multiply(a, 3); // Call to consteval function 'multiply' is not a constant expression
                                // Read of non-const variable 'a' is not allowed in a constant expression
  return 0;
}

constinit Specifier

The constinit specifier ensures that a variable with static or thread storage duration is initialized at compile time. It guarantees that the initialization is either zero initialization or constant initialization.

In C++ Formal Language

If a variable is declared with constinit, its initializing declaration must be applied with constinit. If a variable declared with constinit has dynamic initialization (even if it is performed as static initialization), the program is ill-formed.

If no constinit declaration is reachable at the point of the initializing declaration, the program is ill-formed, no diagnostic required.

constinit cannot be used together with constexpr. When the declared variable is a reference, constinit is equivalent to constexpr. When the declared variable is an object, constexpr mandates that the object must have static initialization and constant destruction and makes the object const-qualified, however, constinit does not mandate constant destruction and const-qualification. As a result, an object of a type which has constexpr constructors and no constexpr destructor (e.g. std::shared_ptr<T>) might be declared with constinit but not constexpr.

Got that?

The constinit specifier is used to mark variables that must be initialized with compile-time constant expressions or constant-initialized constructors and it also ensures that the initialization will be done during the static initialization phase. It prevents the variables with static storage duration to be initialized at runtime. The variables specified with constinit specifier need to be initialized with a constant expression.

//
//  main.cpp
//  const, volatile, mutable
//
//  Created by Bryan Higgs on 2/10/25.
//

consteval int get_value() 
{
  return 42; // The "answer"
}

constinit int value1 = get_value(); // OK: Initialized at compile time
constexpr int value2 = 100;
constinit int value3 = value2; // OK: Initialized at compile time using constexpr
constinit constexpr int value4 = value2; // Cannot combine with previous 'constinit' declaration specifier


int main(int argc, const char * argv[])
{
  static constinit int value5 = 123; // OK: static storage duration
  
  return 0;
}
Index