Skip to content
VLSI Mentor

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

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
    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:

CodeResult
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:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
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:

Azvya Education Pvt. Ltd.VLSI Mentor
unusable.cpp — complete program
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());
}
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
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 const

Neither 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:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
errors: 0
ENABLE is 1 bits

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

Azvya Education Pvt. Ltd.VLSI Mentor
prop.cpp
class Holder {
public:
    void tryAppend() const { s_.append("x"); }
private:
    std::string s_;
};
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
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 const member function, every data member is const. So a const member function can only call const member 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.

Azvya Education Pvt. Ltd.VLSI Mentor
ovl.cpp — the type
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:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
b.at(0):    non-const at()
cb.at(0):   const at()
after b.at(0) = 99, cb.at(0) = 99

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

Azvya Education Pvt. Ltd.VLSI Mentor
mut.cpp
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:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
three calls on a const object; reads_ = 3
sizeof(Cached) = 8

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

Azvya Education Pvt. Ltd.VLSI Mentor
hole.cpp
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;
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
diagnostics: 0
after writing through a const object's data(): b.at(0) = 99

A 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

  1. Mark every member function that does not modify the object const — it qualifies this (Chapter 11.6).
  2. Measured: calling a non-const member on a const object or through a const reference gives 'this' argument … has type 'const Reg', but function is not marked const.
  3. Measured: a const member assigning to a member gives cannot assign to non-static data member within const member function.
  4. The real cost is usability: a type with unmarked observers produced 2 errors in a perfectly ordinary const& function, and 0 once marked.
  5. So every const T& parameter becomes impossible, which is Chapter 6.5's default form.
  6. const propagates into members: s_.append("x") gave no matching member function for call to 'append' because s_ is a const std::string.
  7. So a const member can only call const members of its members — which makes the discipline transitive.
  8. Measured: overload resolution selects on const-ness — b.at(0) chose the non-const version, cb.at(0) the const one.
  9. The const overload should return a const reference, and both should be provided.
  10. const means "does not modify this object's members", not "no effects"; a pointer member's target is unprotected (Chapter 8.7).
  11. mutable exempts one member, for unobservable state only — measured modifying reads_ to 3 on a const object.
  12. mutable breaks the thread-safety assumption about const members (Module 26).
  13. Measured hole: a const member returning a const_cast pointer let a const object be modified with 0 diagnostics.
  14. Write const first 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 qualifies this.
  • Measured: calling a non-const member on a const object or through a const reference is an error naming 'this' argument.
  • The real cost is usability: 2 errors → 0 by adding one keyword twice; without it, every const T& parameter is impossible.
  • const propagates into members, so a const member can only call const members — the discipline is transitive.
  • Measured: overload resolution picks by const-ness; provide both accessors, the const one returning a const reference.
  • const means "does not modify this object's members" — not "no effects".
  • mutable exempts one member for unobservable state; measured working on a const object, and it breaks the thread-safety assumption.
  • Measured hole: a const_cast out of a const member modified a const object with 0 diagnostics.
  • Write const first — 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.