C++ Part I · Module 11 · Classes and Encapsulation
const Member Functions and a const-Correct Interface
The previous chapter explained what const on a member function means — a statement about the hidden argument, which is why it sits in that odd position after the parameter list — and stopped short of saying why it matters. The usual framing is that it documents intent and lets the compiler catch accidental modification, both of which are true and neither of which conveys the actual stakes. Chapter 6.5 established passing by const reference as the default for anything that is not a cheap scalar, and Chapter 9.6 measured what the alternative costs in heap allocations. So the position most of a type's callers will want to hold it in is a const reference, and a type whose observers are not marked const cannot be read through one at all: not awkwardly, not with a cast, simply not. That is measured here as two compiler errors on an entirely reasonable function, which disappear when one keyword is added twice. From there the chapter covers the mechanics worth knowing — that const propagates into member objects, so a const member function can only call other const member functions, which is what makes the discipline compose rather than stopping at one level; that two functions can share a name and be chosen by whether the object is const; and the one deliberate escape hatch. It ends with the hole, which produces no diagnostic at all.
Intermediate17 min readC++Part IFundamentalsClasses
Part I · Module 11 · Chapter 11.7 · const Member Functions and a const-Correct Interface
1. The Rule
class Reg {
public:
int width() const { return width_; } // does not modify: const
void widen() { ++width_; } // modifies: not const
private:
int width_;
};Mark every member function that does not modify the object
const.
Chapter 11.6 §7 explained the mechanism: const qualifies this, making it const Reg* instead of Reg*. This chapter is why it matters, and the reason is not primarily about catching mistakes.
2. The Four Combinations
Measured, each compiled separately:
| Code | Result |
|---|---|
Reg r(4); r.widen(); r.width(); | clean |
const Reg r(4); r.width(); | clean |
const Reg r(4); r.widen(); | error: 'this' argument to member function 'widen' has type 'const Reg', but function is not marked const |
Reg r(4); const Reg& cr = r; cr.widen(); | the same error |
And a const member function attempting to modify:
error: cannot assign to non-static data member within const member function 'width'The first three rows are what everyone expects. The fourth row is where this chapter's stakes live, because it is the form that occurs in real code.
3. The Real Cost: The Type Becomes Unusable
A type whose observers are not marked, and an entirely ordinary function that wants to read it:
class BadReg {
public:
BadReg(std::string n, int w) : name_(std::move(n)), width_(w) {}
int width() { return width_; } // NOT const
const std::string& name() { return name_; } // NOT const
private:
std::string name_;
int width_;
};
// Chapter 6.5's default way to pass anything non-trivial:
static void report(const BadReg& r)
{
std::printf("%s is %d bits\n", r.name().c_str(), r.width());
}errors: 2
error: 'this' argument to member function 'name' has type 'const BadReg',
but function is not marked const
error: 'this' argument to member function 'width' has type 'const BadReg',
but function is not marked constNeither observer can be called. report cannot be written at all — not awkwardly, not with a workaround, not at all. The same source with const added to both observers:
errors: 0
ENABLE is 1 bitsTwo keywords took the type from unusable to usable.
This is why the chapter is rated Critical, and why Chapter 11.6 §7 insisted the fourth row of §2 was the important one. The consequences compound:
Every function taking const BadReg& is impossible, which Chapter 6.5 §3 established as the default parameter form for anything non-trivial.
So callers must take it by value or by non-const reference. By value copies — Chapter 9.6 measured a std::string copy costing a heap allocation in four of five cases. By non-const reference is a lie about intent and prevents passing a temporary.
And const objects of the type are useless. A const BadReg can be created and then not read.
It also spreads. A member function of another class that holds a BadReg and wants to be const itself cannot be, because it cannot call BadReg's observers — which is §4.
4. const Propagates Into Members
The fact that makes the discipline compose, rather than stopping at the first level.
class Holder {
public:
void tryAppend() const { s_.append("x"); }
private:
std::string s_;
};error: no matching member function for call to 'append'The message is worth decoding, because it does not say "you cannot modify". Inside a const member function, this is const Holder*, so the member s_ is a const std::string — and append is not one of std::string's const member functions, so there is no viable overload. The error is about overload resolution, which is the honest description of the mechanism.
Inside a
constmember function, every data member isconst. So aconstmember function can only callconstmember functions of its members.
Two consequences:
The discipline is transitive. If std::string had not marked its observers const, nothing holding a std::string could have const member functions either. The standard library is rigorous about this, and that is what lets your types be.
And it is why one missing const is not a local problem. Chapter 11.3 §8 argued for a small interface because every public member is a promise; const-ness is a promise that propagates outward to every type that contains yours.
5. One Name, Two Functions
const-ness participates in overload resolution, which is what lets a container offer both read and write access under one name.
class Buf {
public:
int& at(int i) { return d_[i]; } // non-const object
const int& at(int i) const { return d_[i]; } // const object
private:
int d_[4] = {1,2,3,4};
};Measured, with b non-const and cb a const Buf& bound to it:
b.at(0): non-const at()
cb.at(0): const at()
after b.at(0) = 99, cb.at(0) = 99The compiler chose by the const-ness of the object. Chapter 6.7 covered overload resolution generally; this is the one factor that chapter deferred.
Three points:
The const version returns const int& so a caller holding a const object cannot write through the result. Returning plain int& from the const overload is §9's hole.
The last line confirms they are the same object — writing 99 through b was visible through cb.
And this is exactly how the standard library works. Chapter 9.5's std::string has both operator[] forms, and Chapter 19.2's containers will too. Providing both is the normal design for any type with element access; providing only the non-const one makes the type unusable per §3.
6. What the Compiler Enforces, and What It Does Not
With observers marked, the compiler checks two things:
That a const member function does not assign to a member — error: cannot assign to non-static data member within const member function.
And that only const members are callable on a const object or reference — §2's third and fourth rows.
What it does not check is whether a function marked const is logically an observer. const means "does not modify this object's members", not "has no effects". A const member function may write to a file, print, increment a global, or modify an object reached through a pointer member — because the pointer is const inside the function, not what it points at. Chapter 8.7 measured that distinction precisely for const int* versus int* const, and it applies unchanged here.
7. The Escape Hatch
Occasionally a member genuinely must change on a logically unchanged object — a cached result, a call counter, a lazily computed value.
class Cached {
public:
int value() const { ++reads_; return v_; } // mutable makes this legal
int reads() const { return reads_; }
private:
int v_ = 7;
mutable int reads_ = 0;
};Measured, on a const object:
three calls on a const object; reads_ = 3
sizeof(Cached) = 8mutable exempts one member from the const-ness of the enclosing object. Three rules for using it:
Use it only where the change is invisible to callers. A cache that cannot be observed, a statistic. If a caller can tell the difference, the function is not an observer and should not be const.
It is per-member, which is right — it is a property of the member, not an exemption for the function.
And it interacts with concurrency. A const member function is widely assumed to be safe to call from several threads at once; one that mutates a mutable member is not, unless it is synchronised. Module 26 is where that matters, and it is the main reason to be sparing with mutable.
8. The Hole
The honest limit, measured.
class Buf {
public:
int* data() const { return const_cast<int*>(d_); } // deliberately wrong
int at(int i) const { return d_[i]; }
private:
int d_[4] = {1,2,3,4};
};
const Buf b;
int* p = b.data();
p[0] = 99;diagnostics: 0
after writing through a const object's data(): b.at(0) = 99A const object was modified, and nothing warned. The const member function handed out a writable pointer to its own data, and from there Chapter 8.2's ordinary pointer write did the rest.
Two conclusions:
const-correctness is a discipline the compiler helps with, not a guarantee it provides. The checks in §6 are real and they cover accidental modification; they do not cover a type that deliberately gives its data away. This is the same shape as Chapter 11.5 §7's int& widthRef() and Chapter 11.4 §8's memcpy — the mechanism protects against accident, and a type can always undermine itself.
So the rule is a design rule: a const member function must not return a non-const pointer or reference to a member. Provide the two overloads of §5 instead, with the const one returning const int&. The const_cast in that listing is the giveaway — Chapter 4.10 §4's argument applies, which is that a cast makes a conversion visible, and a const_cast in a const member function is visibly wrong.
9. What to Actually Do
Mark every member function that does not modify the object const. Write it first and remove it if the compiler objects.
Assume your callers will hold a const reference, because Chapter 6.5 made that the default.
Provide both overloads for element access, the const one returning a const reference.
Never return a non-const pointer or reference to a member from a const member function. Measured: no diagnostic, and the object was modified.
Treat a const_cast in a member function as a review finding.
Use mutable only for genuinely unobservable state, and remember the concurrency implication.
Remember const means "does not modify this object", not "has no effects" — a pointer member's target is not protected.
And repeat the const on out-of-class definitions — Chapter 11.3 §3, it is part of the signature.
10. Mistakes Worth Making Once
Forgetting const on an observer. Measured: 2 errors in the caller, and the type is unusable by const reference.
Adding const only when something fails to compile. The failure arrives far away and late, in someone else's file.
Providing only the non-const element accessor. Half your callers cannot use it.
Returning int& from a const member. The hole: 0 diagnostics, object modified.
Using const_cast to make a const member compile. The design is wrong, not the keyword.
Expecting const to protect what a pointer member points at. It protects the pointer, per Chapter 8.7.
Overusing mutable so that const stops meaning anything, and quietly breaking the thread-safety assumption.
Forgetting the const on the out-of-class definition. It does not match the declaration.
Expecting a runtime benefit. There is none; the benefit is interface usability and accident-prevention.
11. Interview Relevance
"What does const on a member function mean?" That this is a pointer to const, so the function may not modify the object's members. Chapter 11.6's explanation of the position is the follow-up that distinguishes a real understanding.
"Why does it matter?" The strong answer is §3's, not "it documents intent": a type whose observers are unmarked cannot be passed by const reference at all, which is the default parameter form, so the type is unusable rather than merely under-documented. The measured two-errors-to-zero is the evidence.
"Can a const member function modify anything?" It can modify mutable members, objects reached through pointer members, globals, and the outside world. const is about this object's members, not about effects.
"Why do containers have two operator[]?" So the type works for both const and non-const objects, with the const version returning a const reference. Overload resolution selects on the const-ness of the object.
"What is mutable for, and what is the catch?" Unobservable state like a cache or counter — and the catch is that const member functions are widely assumed thread-safe, which a mutable write breaks.
"Does const help performance?" Not meaningfully for member functions. Candidates who claim large optimisation benefits are usually overstating it.
12. What You Should Be Able to Say Now
- Mark every member function that does not modify the object
const— it qualifiesthis(Chapter 11.6). - Measured: calling a non-
constmember on aconstobject or through aconstreference gives'this' argument … has type 'const Reg', but function is not marked const. - Measured: a
constmember assigning to a member givescannot assign to non-static data member within const member function. - The real cost is usability: a type with unmarked observers produced 2 errors in a perfectly ordinary
const&function, and 0 once marked. - So every
const T¶meter becomes impossible, which is Chapter 6.5's default form. constpropagates into members:s_.append("x")gaveno matching member function for call to 'append'becauses_is aconst std::string.- So a
constmember can only callconstmembers of its members — which makes the discipline transitive. - Measured: overload resolution selects on const-ness —
b.at(0)chose the non-constversion,cb.at(0)theconstone. - The
constoverload should return aconstreference, and both should be provided. constmeans "does not modify this object's members", not "no effects"; a pointer member's target is unprotected (Chapter 8.7).mutableexempts one member, for unobservable state only — measured modifyingreads_to 3 on aconstobject.mutablebreaks the thread-safety assumption aboutconstmembers (Module 26).- Measured hole: a
constmember returning aconst_castpointer let aconstobject be modified with 0 diagnostics. - Write
constfirst and remove it if the compiler objects — the asymmetry is that a missing one fails far away and late.
13. Exercises
Exercise 1 — mark them. For a RingBuffer with push, pop, size, empty, capacity, front, clear, say which are const.
Exercise 2 — predict. With class R { public: int w() const; void bump(); }; which compile: (a) R r; r.w(); (b) const R r; r.w(); (c) const R r; r.bump(); (d) R r; const R& c = r; c.w(); (e) R r; const R& c = r; c.bump();
Exercise 3 — the propagation. Explain why s_.append("x") inside a const member gives an overload resolution error rather than an assignment error.
Exercise 4 — two overloads. Write both at() overloads for a Buf holding int d_[8], and say what each returns and why.
Exercise 5 — find the hole. Which of these breaks const-correctness, and why: int size() const, int* data() const, const int* data() const, int& at(int) const, void clear() const.
Exercise 6 — mutable or not? Decide for each: a hit counter, a cached checksum, a lazily opened file handle, a "last error" string returned to callers.
Exercise 7 — the asymmetry. Explain why adding const later is more expensive than removing it, and where the error appears in each case.
Exercise 8 — the usability argument. A colleague says const on members is just documentation. Rebut with the measurement and one consequence from §3.
14. Summary
- Mark every non-modifying member function
const— it qualifiesthis. - Measured: calling a non-
constmember on aconstobject or through aconstreference is an error naming'this' argument. - The real cost is usability: 2 errors → 0 by adding one keyword twice; without it, every
const T¶meter is impossible. constpropagates into members, so aconstmember can only callconstmembers — the discipline is transitive.- Measured: overload resolution picks by const-ness; provide both accessors, the
constone returning aconstreference. constmeans "does not modify this object's members" — not "no effects".mutableexempts one member for unobservable state; measured working on aconstobject, and it breaks the thread-safety assumption.- Measured hole: a
const_castout of aconstmember modified aconstobject with 0 diagnostics. - Write
constfirst — a missing one fails far away and late; an unnecessary one fails immediately. - No runtime cost; the value is interface usability and accident-prevention.
Everything in this module so far has been about an object: its members, the hidden argument that identifies it, and which operations may touch it. There is a category of state that does not belong to any object, and the type is still the right place for it — a count of how many objects exist, a limit shared by all of them, a lookup table that would be wasteful to duplicate. Chapter 11.2 §6 said flatly that the class occupies no storage and its objects do, and then noted one exception without naming it. The next chapter is that exception. The mechanism is short and it has one genuinely surprising property: the declaration inside the class is not the definition, and a program that uses such a member without defining it anywhere compiles perfectly and then fails in the linker, which is the same failure shape Chapter 11.3 measured for member functions in headers.
Standards & specifications
- Governing standard
- ISO/IEC 14882 (the C++ language standard)(opens ISO/IEC (JTC 1/SC 22/WG 21) in a new tab)
Defines the C++ language and its standard library — the type system and templates, the object model and object lifetime, initialisation and overload resolution, the memory model and the containers, algorithms and utilities the library provides. Like C's standard it is a FAMILY of editions (C++98/03, C++11, C++14, C++17, C++20, C++23) and the editions are NOT interchangeable: a feature this curriculum relies on may be unavailable, differently specified or merely proposed in another edition, so each lesson names the edition its claim depends on. It does not define any foreign-function interface to a hardware description language, nor what a compiler actually emits, nor platform behaviour on real silicon — those are the implementation's and the device's contracts. Working drafts published by WG21 are freely available and are what most engineers read day to day; a draft is not the published standard.
This page also covers RTL structure, verification approach and debugging technique. Those are engineering practice built on the standard, not requirements the standard itself imposes.
Where this fits
Part of the C++ Programming curriculum.
