const member functions and mutable
A const member function promises not to change the object, and a const object may only call such functions. The keyword after the parameter list is part of the function's type, so the compiler can enforce the promise and the reader can see which calls are read-only.
Lesson 012 applied const to variables, references and pointers. This lesson applies it to a class: which functions may be called on a const object, how to overload an accessor on const, and the one keyword that lets a const function change a member.
A const member function
The const after the parameter list applies to the implicit this pointer: inside the function the object is const, so a write to a member is a compile error. The function can still read every member and call other const functions.
The program declares a const counter and calls total(), which is const. The same object cannot call add(), because add() is not const; the next section shows the overload that a class with a buffer usually adds.
Marking every function that does not write as const is what makes a const object usable. A class with no const functions cannot be passed as const T&, which blocks most of the standard library.
#include <iostream>
class counter
{
public:
void add(int amount)
{
total_ += amount;
}
int total() const
{
return total_;
}
private:
int total_ = 0;
};
int main()
{
counter c;
c.add(40);
c.add(2);
const counter& view = c;
std::cout << view.total() << '\n';
}
g++ -std=c++23 -Wall -Wextra method.cpp -o method
./method
42
Overloading on const
A class can have two functions with the same name and parameters that differ only in constness. The const object picks the const one, a non-const object picks the other, and the compiler chooses from the object it calls through.
The program's shelf has two at() functions: the const one returns a copy, and the non-const one returns a reference so a caller can write through it. That is the shape std::vector uses for operator[] and its at().
The const overload is what makes a read-only view cheap: a function taking const shelf& gets the copy version and cannot modify the shelf, while the owner of the shelf can still change its elements.
#include <iostream>
class shelf
{
public:
explicit shelf(int first) : first_(first) {}
int& at()
{
return first_;
}
int at() const
{
return first_ + 100;
}
private:
int first_;
};
int main()
{
shelf s{1};
s.at() = 7;
const shelf& view = s;
std::cout << s.at() << ' ' << view.at() << '\n';
}
g++ -std=c++23 -Wall -Wextra overload.cpp -o overload
./overload
7 107
mutable
A cache is a member that changes for bookkeeping, not as part of the object's visible state. Declaring it mutable lets a const function write to it, so the cache can be filled on the first read and reused afterwards.
The program's report computes a checksum once and stores it in a mutable member. The const function changes the cache, sets a flag, and returns the same value on the second call without recomputing.
Use mutable for caches, lazy initialisation and debugging counters, and for nothing that a caller could observe as state. If a const function changes something the caller cares about, the function is not really const.
#include <iostream>
class report
{
public:
explicit report(int base) : base_(base) {}
int checksum() const
{
if (!cached_) {
cache_ = base_ * 2 + 1;
cached_ = true;
std::cout << "computed\n";
}
return cache_;
}
private:
int base_;
mutable int cache_ = 0;
mutable bool cached_ = false;
};
int main()
{
const report r{20};
std::cout << r.checksum() << ' ' << r.checksum() << '\n';
}
g++ -std=c++23 -Wall -Wextra mutable.cpp -o mutable_demo
./mutable_demo
computed
41 41
A const function cannot call a non-const one
Inside a const member function the object is const, so calling a member function that is not const is the same mistake as calling it on a const object. The compiler refuses, even though the caller of the outer function was not const.
The program below has a const total() that calls a non-const reset(). The error points at the call inside the class, and the fix is either to make reset() const (if it really does not change the object) or to remove const from total().
This is the compiler enforcing the promise in both directions: a const function may not start a write, and a caller holding a const reference may not either.
class counter
{
public:
int total() const
{
reset();
return 0;
}
void reset()
{
total_ = 0;
}
private:
int total_ = 0;
};
int main()
{
counter c;
return c.total();
}
g++ -std=c++23 -Wall -Wextra call.cpp -o call
call.cpp: In member function 'int counter::total() const':
call.cpp:6:14: error: passing 'const counter' as 'this' argument discards qualifiers [-fpermissive]
6 | reset();
| ~~~~~^~
call.cpp:10:10: note: in call to 'void counter::reset()'
10 | void reset()
| ^~~~~
A non-const function on a const object
Calling a function that may write on an object the caller promised not to change is an error. The compiler reports that the object has a const qualifier and discards it when binding to this.
The program stores a counter as const and then calls add(). The message names the member function and the const object, and the fix is to remove const from the variable, or to add a const function that does the read the code really wanted.
The same message appears when a const object is passed to a function that takes a non-const reference. Const correctness is contagious in both directions, which is why it is cheaper to adopt at the start of a class.
#include <iostream>
class counter
{
public:
void add(int amount)
{
total_ += amount;
}
int total() const
{
return total_;
}
private:
int total_ = 0;
};
int main()
{
const counter c;
c.add(5);
std::cout << c.total() << '\n';
}
g++ -std=c++23 -Wall -Wextra const_object.cpp -o const_object
const_object.cpp: In function 'int main()':
const_object.cpp:23:10: error: passing 'const counter' as 'this' argument discards qualifiers [-fpermissive]
23 | c.add(5);
| ~~~~~^~~
const_object.cpp:6:10: note: in call to 'void counter::add(int)'
6 | void add(int amount)
| ^~~
Exercises
- Mark add() const in method.cpp and try to compile. Which line inside add() does the compiler reject?
- Add a size() function to shelf with a const and a non-const overload, and call both through a const reference.
- Make the cache in mutable.cpp store the number of calls instead of the checksum and print it from a const function.
Questions
A new set of questions in a new order every time. Choose one answer for each, then check.
-
What does a const member function promise?
Answer
It does not modify the object it is called on
The promise is part of the function type, and the compiler enforces it.
-
May a const object call a non-const member function?
Answer
No, the object would lose its const qualifier
A non-const member takes a non-const object, and a const object cannot bind to it.
-
What is
mutablefor?Answer
A member that a const function may still change, such as a cache
The cache does not change the logical value of the object, so const permits it.
-
What does overloading on const allow?
Answer
A const and a non-const version of one function
The const qualifier is part of the signature, and the object decides which version is called.
-
What does this program print?
#include <iostream> class value { public: explicit value(int v) : v_(v) {} int get() const { return v_; } private: int v_; }; int main() { const value v{42}; std::cout << v.get(); }Answer
42
The const member function reads the member of a const object and prints it.
-
What does this program print?
#include <iostream> class box { public: box() : v_(7) {} int get() { return v_; } int get() const { return v_ + 100; } private: int v_; }; int main() { box b; const box c; std::cout << b.get() << ' ' << c.get(); }Answer
7 107
The non-const object calls the non-const overload, and the const object calls the const one.
-
What does this program print?
#include <iostream> class lazy { public: int value() const { if (!ready_) { cached_ = 41; ready_ = true; std::cout << "computed\n"; } return cached_; } private: mutable int cached_ = 0; mutable bool ready_ = false; }; int main() { const lazy l; std::cout << l.value() << ' ' << l.value(); }Answer
computed / 41 41
The first call fills the mutable cache, and both reads return the same value.
-
What does calling a non-const function from a const member report?
Answer
passing 'const counter' as 'this' argument discards qualifiers
Inside a const function the object is const, and the non-const call cannot take it.
-
What does calling
c.add(5)on a const object report?Answer
passing 'const counter' as 'this' argument discards qualifiers
The call needs a non-const object, and the message names the function and the object type.
-
Why is the const qualifier part of a member function's signature?
Answer
So the two versions can be overloaded and selected by the object
The qualifier changes the type of the implicit object parameter, which makes the overloads distinct.
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