const and const correctness
const is a promise written into the type: through this name, the object will not be modified. The compiler enforces the promise, the reader sees it in the signature, and the optimiser may rely on it. A program that is const-correct says which values change and which do not, and most accidental writes are caught at compile time.
The lesson starts with a const variable, moves to the two places const can sit around a pointer, and ends with the errors that a missing or wrong const produces. const reference parameters, which lesson 011 used to avoid copies, are one part of the same idea.
A const variable cannot change
A const variable is initialised once and never assigned again. The initialiser is required: a const int without a value could never be given one. The name can be used anywhere a value is expected, including the bound of a loop.
The compiler knows the value at every use, so a const int costs no storage in most builds and cannot be modified by accident. The program below uses limit as the loop bound and prints the sum.
When the value is needed in a context that requires a compile-time constant, such as an array bound, use constexpr instead: the next section shows the difference.
#include <iostream>
int main()
{
const int limit = 4;
int total = 0;
for (int i = 1; i <= limit; ++i) {
total += i;
}
std::cout << limit << ' ' << total << '\n';
}
g++ -std=c++23 -Wall -Wextra const_var.cpp -o const_var
./const_var
4 10
constexpr values
constexpr means the value must be computable at compile time. A constexpr variable can be used where the language needs a constant expression, and a static_assert can check one without running the program.
The program computes the number of squares of a board from two constexpr ints and checks the result at compile time. If the arithmetic were wrong, the build would stop, not the test run.
constexpr functions and their limits are the subject of lesson 122. For values, constexpr is the stronger form of const: every constexpr variable is const, but not every const is usable in a constant expression.
#include <iostream>
constexpr int board_width = 8;
constexpr int board_height = 8;
constexpr int squares = board_width * board_height;
int main()
{
static_assert(squares == 64, "the board is 8 by 8");
std::cout << squares << '\n';
}
g++ -std=c++23 -Wall -Wextra constexpr.cpp -o constexpr_demo
./constexpr_demo
64
Pointer to const, const pointer
const can sit on either side of the star, and the two positions mean different things. const int* p is a pointer to an int that must not be written through p; the pointer itself can be changed. int* const p is a pointer that cannot be changed; the int it points at can be written.
Read the declaration from right to left: int* const is a const pointer to int, const int* is a pointer to const int. Writing const int* const locks both.
The program writes through the const pointer and through the original variable, then reads through the pointer to const. Both names see 30, because all three refer to one int.
#include <iostream>
int main()
{
int value = 10;
const int* to_const = &value;
int* const const_ptr = &value;
*const_ptr = 20;
value = 30;
std::cout << *to_const << ' ' << *const_ptr << '\n';
}
g++ -std=c++23 -Wall -Wextra pointer_const.cpp -o pointer_const
./pointer_const
30 30
A const reference keeps a temporary alive
Binding a temporary to a const reference extends its lifetime to the scope of the reference. const std::string& ref = std::string("temporary"); keeps the string alive until ref goes out of scope, which is why the read on the next line is safe.
The same rule is what makes a const reference parameter accept a literal: the temporary lives until the end of the full expression that contains the call. A non-const reference gets no such extension, and the compiler refuses to bind one to a temporary at all.
A reference to a temporary is not a licence to return references to locals; the extension applies to the reference that is bound, not to references copied out of the function.
#include <iostream>
#include <string>
int main()
{
const std::string& ref = std::string("temporary");
std::cout << ref << ' ' << ref.size() << '\n';
}
g++ -std=c++23 -Wall -Wextra lifetime.cpp -o lifetime
./lifetime
temporary 9
What the compiler refuses
Two mistakes cover most const errors. The first is a write to a const variable, which the compiler reports as an assignment of a read-only variable. The second is a non-const reference parameter called with a literal, which would allow the function to write into a temporary.
The program below makes both mistakes in one file. GCC stops at the first error and prints the second after it, and neither line can be fixed by a cast that keeps the promise intact: the fix is to remove the write, or to take the parameter by value or by const reference.
A cast that removes const, such as const_cast, is legal only when the object was never really const. In code that keeps its promises it is not needed, and it is not part of this course before the chapter on legacy code.
#include <iostream>
void take(int& value)
{
value = 1;
}
int main()
{
const int limit = 3;
limit = 4;
take(5);
std::cout << limit << '\n';
}
g++ -std=c++23 -Wall -Wextra const_mistakes.cpp -o const_mistakes
const_mistakes.cpp: In function 'int main()':
const_mistakes.cpp:11:11: error: assignment of read-only variable 'limit'
11 | limit = 4;
| ~~~~~~^~~
const_mistakes.cpp:12:10: error: cannot bind non-const lvalue reference of type 'int&' to an rvalue of type 'int'
12 | take(5);
| ^
const_mistakes.cpp:3:16: note: initializing argument 1 of 'void take(int&)'
3 | void take(int& value)
| ~~~~~^~~~~
Exercises
- Make limit constexpr in const_var.cpp. Does the program still build, and what changes in the binary?
- Write a function that takes int* const and one that takes const int*. Call each with the address of a non-const int and try to write through it in both.
- Remove const from the reference in lifetime.cpp and compile. Which line does GCC point at, and why can a temporary not bind to int&?
Questions
A new set of questions in a new order every time. Choose one answer for each, then check.
-
What does
conston a variable promise?Answer
The name will not be used to modify the object
The promise is written into the type and enforced by the compiler at every use of that name.
-
What does
constexpradd to a const variable?Answer
The value must be known at compile time
A constexpr variable takes part in constant expressions, which is why it can size an array.
-
What is the difference between
const int*andint* const?Answer
The first points to a const int, the second is a const pointer
The const applies to what follows it in the declaration, the pointee in the first and the pointer in the second.
-
What does a const reference to a temporary give the program?
Answer
A lifetime for the temporary that matches the reference
The language extends the temporary's lifetime to the end of the reference's scope.
-
What does this program print?
#include <iostream> int main() { const int limit = 4; constexpr int doubled = limit * 2 + 2; std::cout << limit << ' ' << doubled; }Answer
4 10
The const value is 4, and the constexpr value is computed from it at compile time.
-
What does this program print?
#include <iostream> constexpr int cube(int value) { return value * value * value; } int main() { std::cout << cube(4); }Answer
64
The constexpr function computes 4 cubed at compile time, and the value is printed.
-
What does this program print?
#include <iostream> int main() { int value = 30; const int* pointer = &value; int* const fixed = &value; std::cout << *pointer << ' ' << *fixed; }Answer
30 30
Both declarations point at the same int, and the program reads it through each one.
-
What does this program print?
#include <iostream> #include <string> int main() { const std::string& text = std::string("temporary"); std::cout << text << ' ' << text.size(); }Answer
temporary 9
The const reference extends the temporary's lifetime, and its nine characters are printed.
-
What does the compiler say about
limit = 4;when limit is const?Answer
assignment of read-only variable 'limit'
The name is read-only, and the message names the variable.
-
What does the compiler say about
take(5)when take has anint¶meter?Answer
cannot bind non-const lvalue reference of type 'int&' to an rvalue of type 'int'
The literal is a temporary, and a non-const reference cannot bind to it.
The code on this page was built with g++ 15.2.0 and run on Alpine Linux 3.24.3 (x86_64) in a Docker container. The programs whose section shows a compiler error were compiled until that error and were not run.
Article author: Arthur Isaev