Ch13.3: const Member Functions: Promising Not to Modify
1. What const After the Parameter List Means
Every member function receives the object it was called on as an invisible
pointer, this (Ch13.7).
Writing const after the parameter list changes the type of that
pointer:
::std::int_least32_t value() const noexcept; // inside: this is counter const *
void add(::std::int_least32_t n) noexcept; // inside: this is counter *
That is all it does, and it is enough to give you two guarantees at once:
-
Inside the function you cannot assign to a data member and
you cannot call a member function that is not
const. The compiler enforces the promise; it is not a comment. -
Outside the function a
constobject — or any object reached through aconst &— can only callconstmember functions. So a function that takescounter const &physically cannot modify the counter.
2. The Rule: A Read-Only Member Function Is const
Ask one question about every member function you write: does it change the
object? If the answer is no, the declaration ends with
const. That is the rule, and it has no exceptions worth making.
#include <fast_io.h>
#include <cstdint>
using namespace ::fast_io::io;
class counter
{
public:
void add(::std::int_least32_t n) noexcept
{
value_ += n;
}
::std::int_least32_t value() const noexcept
{
return value_;
}
private:
::std::int_least32_t value_{};
};
// A const reference can only call const member functions.
void show(counter const &c) throws
{
println("value = ", c.value());
}
int main()
{
try
{
counter c{};
c.add(5);
show(c);
// A const reference to the same object: a read-only view of it.
counter const &frozen{c};
show(frozen);
println("frozen.value() = ", frozen.value());
}
catch throws (::std::error e)
{
print("failed: ", e, "\n");
}
}
value = 5
value = 5
frozen.value() = 5
show takes counter const & because it only reads.
It calls c.value(), which compiles only because
value() is const. Had value() been
written without const, this perfectly ordinary function would not
compile, and the fix would be on value(), not on
show.
Here is what the compiler says when you try to change a const
object — frozen.add(1):
error: passing 'const counter' as 'this' argument discards qualifiers [-fpermissive]
note: in call to 'void counter::add(int_least32_t)'
“discards qualifiers” means the call would throw away the
const, and the compiler is not going to let you do that by
accident.
const_cast to silence this.
If you need to write inside a const member function, then either
the function is not really read-only (remove its const, and accept
that it cannot be called on a const object) or the object you were
handed should not have been const in the first place. Both of
those are information about your design; the cast throws the information away
and leaves a program that lies.
3. Why This Is the Most Valuable Habit in the Chapter
const is contagious in the best way. Once the read-only member
functions are marked, const & parameters appear everywhere,
and every function in the program documents — and the compiler checks
— who may change what. Three concrete consequences:
-
A
const ¶meter is a promise you can trust. When you callshow(c), you know the counter comes back the way it went in. You do not have to readshow; you read its signature. -
Errors are caught where they are made. A missing
constis a compile error at the call site, not a wrong number three functions later. -
Interfaces stay honest. A member function without
constannounces “I may modify this object”. Readers of the class can then look at the short list of modifiers and know where the state changes.
const,
are two different overloads.
That is occasionally useful — a mutable accessor and a read-only one:
::std::int_least32_t &value() noexcept { return value_; }
::std::int_least32_t value() const noexcept { return value_; }
The compiler picks by the object: a mutable counter gets the first,
a const one gets the second. Use this deliberately, not by
accident: handing out a mutable reference to a private data member gives away
the control you gained in Ch13.5.
4. const Member Functions and throws
The two specifications are independent and both appear at the end of the
declaration, const first:
::std::int_least32_t value() const noexcept; // read, cannot fail
void report() const throws; // read, may fail (it prints)
In the example above show is throws rather than
noexcept because it prints, and its caller in main
wraps the call in try { } catch throws (::std::error e). The class
itself had to do nothing to make that work: a member function’s failure
travels out of c.value(), then out of show, and is
caught where the program is ready to deal with it
(Ch3.16).