Constants

C++ defines a complete set of methods for defining read-only values. Variables qualified with const are read-only; the compiler performs conflict checks at compile time to prevent modification of read-only values and may perform some optimizations.

In general, variables and parameters should be qualified with const whenever possible to improve code robustness.

The const Type Qualifier

Constants

A variable qualified with const cannot have its value changed after initialization.

const int a = 0;  // a has type const int

// a = 1; // cannot modify a constant

Constant References and Constant Pointers

Constant references and constant pointers both restrict modification of the value to which they point.

int a = 0;
const int b = 0;

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

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

It is also necessary to distinguish a constant pointer (const t*) from a pointer constant (t* const), as in the following declarations:

int* const p1;  // pointer constant; after initialization, the pointed-to address cannot change, but the pointed-to value can be changed
const int* p2;  // constant pointer; the dereferenced value cannot change, but it can point to another int variable
const int* const p3;  // constant pointer constant; the value cannot change, and the pointed-to address cannot change

// 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 modified by mistake and improve code readability.

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

const Member Functions

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

#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(); // a const member function cannot call a non-const member function
    constFunc1();

    // s = ss; // a const member function cannot modify a member variable
  }
};

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

Constant Expression constexpr (C++11)

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

Compile-time computation allows better optimization, such as hard-coding the result into assembly and eliminating the cost of runtime computation. Unlike the optimization brought by const, when a variable qualified with constexpr satisfies the conditions for a constant expression, the compiler is forced to calculate its result at compile time rather than at runtime.

A more intuitive way to understand this is to think of const as “read-only” and constexpr as “immutable”
constexpr int a = 10;  // directly define a constant

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

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

The following example clearly illustrates the difference between const and constexpr. The code uses recursion to calculate the Fibonacci sequence and uses control flow to output the result.

Implementation
#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)
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` performs 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 calculated 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 # the variable value is read
        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-qualified fib0 function uses a constant argument at its only call site, so the entire function runs only at compile time. Since the function does not execute at runtime, the compiler determines that it does not need to generate assembly code.

At the same time, note that there is no initialization code for v0 in the assembly. In the code that calls cout to output v0, v0 has been replaced by the final calculated result, showing that the variable's value was obtained at compile time and runtime computation was optimized away. The initialization of v1, on the other hand, is still an ordinary recursive call to fib1.

Thus, constexpr can be used to replace constants defined by macros and avoid the risks of macro definitions.

In algorithm problems, constexpr can be used to store variables with small data sizes and eliminate the corresponding runtime computation cost. It is especially common in the “precomputation table” technique, where arrays and other containers storing answers are qualified with constexpr.

Excessive Compile-Time Computation Can Cause a Compilation Error

The compiler limits the cost of compile-time computation. If the computation is too large, compilation will fail; consider using const instead.

#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 Reported by Clang When Using constexpr
:10:20: error: constexpr variable 'v' must be initialized by a constant expression
    10 |     constexpr auto v = fib(32);
    |                    ^   ~~~~~~~~~~~~
:6:25: note: constexpr evaluation exceeded maximum depth of 512 calls
    6 |     return i <= 2 ? i : fib(i - 2) + fib(i - 1);
    |                         ^
:6:25: note: in call to 'fib(32)'
    6 |     return i <= 2 ? i : fib(i - 2) + fib(i - 1);
    |                         ^~~~~~~~~~
:6:25: note: in call to ...

References

This content comes from OI Wiki: Constants. See the original for authors and revision history. AIOJ has adapted the formatting, in-site links, and resource paths.

Content is shared under CC BY-SA 4.0 and SATA license; additional terms are governed by the original notice. Thanks to the OI Wiki authors and contributors; per the SATA terms, please visit the OI Wiki repository and star it to show your support.