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 ...