Skip to content

Constants

C++ defines a complete set of methods for defining read-only quantities. Variables modified by const are read-only quantities. The compiler performs conflict checks at compile time to prevent modifications to read-only quantities and may also perform some optimizations.

In general, you should use const to modify variables and parameters as much as possible to improve code robustness.

const Type Qualifier

Constants

Variables modified by const cannot have their values changed after initialization.

1
2
3
const int a = 0;  // a has type const int

// a = 1; // Cannot modify a constant

Constant References and Constant Pointers

Both constant references and constant pointers restrict modification of the pointed-to value.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
int a = 0;
const int b = 0;

int *p1 = &a;
*p1 = 1;
const int *p2 = &a;
// *p2 = 2; // Cannot modify variable through constant pointer
// int *p3 = &b; // Cannot use int* to point to const int variable
const int *p4 = &b;

int &r1 = a;
r1 = 1;
const int &r2 = a;
// r2 = 2; // Cannot modify variable through constant reference
// int &p3 = b; // Cannot use int& to reference const int variable
const int &r4 = b;

Additionally, it's important to distinguish between a constant pointer (const t*) and a pointer constant (t* const). For example, the following declarations:

1
2
3
4
5
6
7
8
int* const p1;  // Pointer constant: address pointed to cannot be changed after initialization, but the value can be changed.
const int* p2;  // Constant pointer: dereferenced value cannot be changed, but can point to other int variables.
const int* const p3;  // Constant pointer constant: value cannot be changed, and address pointed to cannot be changed.

// Using aliases can improve readability.
using const_int = const int;
using ptr_to_const_int = const_int*;
using const_ptr_to_const_int = const ptr_to_const_int;

Using const to qualify parameter types in function parameters can prevent variables from being accidentally modified and increase code readability.

1
2
3
4
void sum(const std::vector<int> &data, int &total) {
  for (auto iter = data.begin(); iter != data.end(); ++iter)
    total += *iter;  // iter is an iterator, the dereferenced type is const int
}

const Member Functions

Member functions qualified with const within a type can be used to restrict modification of members.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
#include <iostream>

struct ConstMember {
  int s = 0;

  void func() { std::cout << "General Function" << std::endl; }

  void constFunc1() const { std::cout << "Const Function 1" << std::endl; }

  void constFunc2(int ss) const {
    // func(); // const member function cannot call non-const member function
    constFunc1();

    // s = ss; // const member function cannot modify member variables
  }
};

int main() {
  int b = 1;
  ConstMember c{};
  const ConstMember d = c;
  // d.func(); // Constant cannot call non-const member function
  d.constFunc2(b);
  return 0;
}

Constant Expression constexpr (C++11)

A constant expression is an expression whose result can be computed at compile time. constexpr requires the compiler to be able to evaluate the value of a function or variable at compile time.

Compile-time computation allows for better optimization, such as hardcoding results into assembly and eliminating runtime computation overhead. Unlike optimizations brought by const, when a variable modified by constexpr meets the conditions of a constant expression, it forces the compiler to compute the result at compile time rather than runtime.

A more intuitive understanding is to think of const as 'read-only' and constexpr as 'immutable'.
1
2
3
4
5
6
7
8
constexpr int a = 10;  // Directly define a constant

constexpr int FivePlus(int x) { return 5 + x; }

void test(const int x) {
  std::array<x> c1;            // Error, x is unknown at compile time
  std::array<FivePlus(6)> c2;  // Feasible, FivePlus is known at compile time
}

The following example clearly illustrates the difference between const and constexpr. The code uses recursion to compute the Fibonacci sequence and outputs using control flow.

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

using namespace std;

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

unsigned fib1(unsigned n) { return n <= 1 ? 1 : (fib1(n - 1) + fib1(n - 2)); }

int main() {
  constexpr auto v0 = fib0(9);
  const auto v1 = fib1(9);

  cout << v0;
  cout << ' ';
  cout << v1;
}
Possible assembly code after compilation (using Compiler Explorer, Clang 19)
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
fib1(unsigned int):
        push    r14
        push    rbx
        push    rax
        mov     ebx, 1
        cmp     edi, 2
        jb      .LBB0_4
        mov     r14d, edi
        xor     ebx, ebx
.LBB0_2:
        lea     edi, [r14 - 1]
        call    fib1(unsigned int)
        add     r14d, -2
        add     ebx, eax
        cmp     r14d, 1
        ja      .LBB0_2
        inc     ebx
.LBB0_4:
        mov     eax, ebx
        add     rsp, 8
        pop     rbx
        pop     r14
        ret

main:
        push    r14
        push    rbx
        push    rax
        mov     edi, 9
        call    fib1(unsigned int) # Initialization of `v1` involves a function call
        mov     ebx, eax
        mov     r14, qword ptr [rip + std::__1::cout@GOTPCREL]
        mov     rdi, r14
        mov     esi, 55 # `v0` is replaced by the final computed result
        call    std::__1::basic_ostream<char, std::__1::char_traits<char>>::operator<<(unsigned int)@PLT
        mov     byte ptr [rsp + 7], 32
        lea     rsi, [rsp + 7]
        mov     edx, 1
        mov     rdi, r14
        call    std::__1::basic_ostream<char, std::__1::char_traits<char>>& std::__1::__put_character_sequence[abi:ne200000]<char, std::__1::char_traits<char>>(std::__1::basic_ostream<char, std::__1::char_traits<char>>&, char const*, unsigned long)
        mov     rdi, r14
        mov     esi, ebx # Reads the variable value
        call    std::__1::basic_ostream<char, std::__1::char_traits<char>>::operator<<(unsigned int)@PLT
        xor     eax, eax
        add     rsp, 8
        pop     rbx
        pop     r14
        ret

The constexpr-modified fib0 function is called with a constant argument at its only call site, causing the entire function to run only at compile time. Since the function has no runtime execution, the compiler determines that no assembly code needs to be generated.

Also note in the assembly that v0 has no initialization code. In the code that calls cout to output v0, v0 has already been replaced by the final computed result, indicating the variable value was computed at compile time, optimizing away runtime computation. In contrast, the initialization of v1 is still a normal recursive call to fib1.

Therefore, constexpr can be used to replace macro-defined constants, avoiding the risks of macro definitions.

In algorithmic problems, constexpr can be used to store variables with small data sizes to eliminate corresponding runtime computation overhead. It is particularly common in the "table lookup" technique, using constexpr-modified containers like arrays to store answers.

Excessive compile-time computation will cause compilation errors

The compiler limits the overhead of compile-time computation. If the computation is too large, compilation will fail, and const should be considered instead.

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

using namespace std;

constexpr unsigned long long fib(unsigned long long i) {
  return i <= 2 ? i : fib(i - 2) + fib(i - 1);
}

int main() {
  // constexpr auto v = fib(32); evaluation exceeded maximum depth
  const auto v = fib(32);
  cout << v;
  return 0;
}
Compilation error given by Clang when using constexpr
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
<source>:10:20: error: constexpr variable 'v' must be initialized by a constant expression
    10 |     constexpr auto v = fib(32);
    |                    ^   ~~~~~~~~~~~~
<source>:6:25: note: constexpr evaluation exceeded maximum depth of 512 calls
    6 |     return i <= 2 ? i : fib(i - 2) + fib(i - 1);
    |                         ^
<source>:6:25: note: in call to 'fib(32)'
    6 |     return i <= 2 ? i : fib(i - 2) + fib(i - 1);
    |                         ^~~~~~~~~~
<source>:6:25: note: in call to ...

References