Skip to content

Module 4 — Misconceptions and Mistakes

Misconceptions are wrong mental models: code that compiles, looks right, and is wrong. Module 4 has more of these than any module so far, because the failure it teaches produces no message at all. Mistakes produce messages, and they are the shorter list.


Part 1: Misconceptions

M1. "Private means encapsulated"

Private is one third of encapsulation. The other two thirds are every public operation preserves the invariant and the interface expresses what the system does.

class Member {
public:
    void setBorrowedCount(int count) { borrowedCount_ = count; }   // private field,
private:                                                          // public rule gone
    int borrowedCount_ = 0;
};

Every member is private, and the rule "0 ≤ count ≤ 3" is enforced nowhere. private stops a caller naming the member; it says nothing about whether the operations available let them break the rules. The test is not "is the data hidden?" but "can a caller reach a state my invariant forbids?"

M2. "Getters and setters are encapsulation"

This is M1 wearing a tie, and it is the most common structure in student code. A class with one getter and one setter per field has the syntax of encapsulation and none of the property, because callers can still reach the representation — one function call later.

The rule: a public member should express an operation of the system, not an operation on a field. canBorrowMore(), lend(), applyRaise() — not getCount(), setOnLoan(), setSalary(). §4 of Concepts has the conversion table.

Getters are not banned. A read-only fact with no rule attached (Book::id()) is fine. Setters are rarer than beginners think, and each one deserves the question: what would a caller do with this that the class could not do for them?

M3. "Encapsulation costs performance"

No. canBorrowMore() const defined inside the class body is implicitly inline, and every compiler in ordinary use inlines it into a comparison of two integers. The generated code is identical to reading a public field. (This is true of accessor functions; it is not an argument for accessor-shaped designs.)

This is worth knowing, because the misconception is what makes people reach for public fields. If your reason for a public field is speed, measure it — you will not find it. If your reason is convenience, that is a design decision, and it is worth saying out loud.

M4. "const means the object cannot change"

A const object cannot be modified. A const member function is a promise about one function: calling this will not modify the object's observable state.

The difference matters because the two are used in opposite places. const on an object is about a variable's permissions; const on a member function is about what a caller may assume. And this distinction has a sharp edge:

const Basket order("Nadia");
order.add("C++ Primer");          // error: discards qualifiers

Two legitimate fixes — mark nothing differently and accept that this object is read-only, or change the variable to be non-const if it is genuinely going to change. A third, const_cast, means lying to the compiler about a promise.

M5. "mutable is a hack"

It is a narrow tool with a real use: a member that is not part of the object's observable state — a cache, a memoised result, a lock, a diagnostic counter.

class Report {
public:
    std::string text() const {             // const, and correctly so
        if (!built_) { cache_ = buildSlowly(); built_ = true; }
        return cache_;
    }
private:
    mutable std::string cache_;            // the caller cannot observe this change
    mutable bool built_ = false;
};

The test: can a caller tell that this changed? If the answer is yes — if the object's answers, output or behaviour differ — then the function is not const, and mutable is being used to silence the compiler rather than to describe the truth.

mutable on a piece of state that is observable is worse than a public field, because the interface now claims something false.

M6. "friend breaks encapsulation, so never use it"

friend grants one specific function or class access to private members. It is idiomatic for operator<< (Module 5), where there is genuinely no other way to reach the members — and where the friend is your own function, in your own header.

What friend does cost is coupling: the friend knows your representation, so it must be re-examined every time the representation changes. The question is not "is friend bad?" but "is this the smallest coupling that solves the problem?"

Before adding one: does the operation belong in the class (then it needs no friend)? Could a public query replace it (isLongRead() instead of pages())? Is it a formatter or a comparison (then it is the ordinary case)? A friend added because something would not compile is a design decision made by accident.

M7. "Information hiding is about security"

There is no security here. private has no runtime effect whatsoever — a compiled program with private members is byte-for-byte the same as one with public members, and a determined programmer can reach the bytes anyway.

Information hiding is about change. A caller cannot depend on what it cannot see, so a private representation may be replaced freely. That is why the representation-swap test is the real measure: not "is it hidden?" but "can I change it without breaking anyone?".

M8. "Validate the input where it arrives"

This one is subtle and expensive. Validation at the point of arrival feels thorough, and it puts each rule in as many places as there are callers — which means it will eventually be missing from one of them, and that one will be discovered by a user.

examples/module-04/01-broken-encapsulation/main.cpp is the proof. alice ends up with minus two items, and the function that trusts the count returns a confident, meaningless yes. The damage is done at the call site, and it surfaces somewhere else entirely.

The rule: a rule belongs in the type that owns the data, exactly once. A caller may validate too — for a good error message, early exit, or a user interface — but the type's version is the one that makes the invariant true.


Part 2: Mistakes, and the two silences

Module 4's characteristic failures are mostly silent, which is why this section is short and the previous one long. The messages below were produced by compiling the code shown, with the course's own flags.

D1 — calling a non-const member on a const object

const Basket order("Nadia");
order.add("C++ Primer");
error: passing 'const Basket' as 'this' argument discards qualifiers [-fpermissive]
note:   in call to 'void Basket::add(const std::string&)'

The compiler is telling you that this would have to be a const Basket* and your function does not accept one. Two legitimate responses: mark add const (only if it truly does not modify), or accept that the object should not have been const.

D2 — a const member function that hands out mutable access

std::vector<std::string>& itemsConst() const { return items_; }
error: binding reference of type 'std::vector<std::__cxx11::basic_string<char> >&' to
       'const std::vector<std::__cxx11::basic_string<char> >' discards qualifiers

The mangled spelling std::__cxx11::basic_string<char> is GCC's internal name for std::string; it appears in messages and is nothing to worry about. The message's meaning is simple: inside a const member function, the member is const, and you cannot hand out a non-const reference to it.

This error is a gift. The compiler has caught the leak from §5 of Concepts before it shipped.

D3 — assigning to a member from a const member function

error: assignment of read-only member 'Basket::itemCount_'

Same cause, different symptom: the member is const because this is a const Basket*. If the function is genuinely supposed to modify the object, it should not be const. If the state is a cache the caller cannot observe, mutable is the answer (M5).

D4 — the silence: a mutable-reference getter

class Basket {
public:
    std::vector<std::string>& items() { return items_; }     // no `const`, so: compiles
private:
    std::vector<std::string> items_;
};

int main() {
    Basket basket;
    basket.items().push_back("sneaked in behind the invariant");   // compiles cleanly
    return 0;
}

No error. No warning. The -Wall -Wextra -Werror build accepts both lines, and every rule the Basket was supposed to enforce is now optional. This is the failure mode Module 4 exists to teach, and it is why the module's central tool is a test rather than a compiler flag — see Debugging.

D5 — the second silence: a setter that validates nothing

void setBorrowedCount(int count) { borrowedCount_ = count; }

Nothing in a compiler can object to this: the types are fine, the name is fine, the class compiles. The mistake is a design mistake, and the only detector is the question from M1: can a caller reach a state my invariant forbids?

D6 — friend used to make something compile

friend class Catalogue;      // added at 02:40, no comment, no reason

No error, ever. The cost is that Catalogue may now write your members directly, so every future change to the representation must be co-ordinated with it — informally, invisibly, and without the compiler's help. If this line appears in your code, it should have a comment saying which operation needed it and why that operation is not a member of the class.


The two-minute check for any class you write

  1. State the invariant in one sentence. If there is none, fine — but say so deliberately.
  2. Try to break it from outside. Write the offending line. If it compiles, the invariant is not enforced. (This is the whole technique of the next page.)
  3. Count the public members. For each: what caller's sentence justifies it? Any member whose sentence ends nowhere is a deletion.
  4. Look for the four leaks. A returned non-const reference, a returned iterator or pointer, a signature naming a container or capacity, a const& return whose lifetime the caller can outlive.
  5. Mark every const member function. Then check the reverse: is anything const that actually modifies observable state? That is where const_cast and mutable mistakes live.
  6. Find the refusal behaviour. Can any operation fail? If so, what happens to the object when it does — and can the caller tell which failure it was?
  7. Say what a caller cannot do. The best classes have a sentence of the form "there is no way for a caller to …". If you cannot write one, the interface is a public representation.

Next: Debugging — the attack test, and the escalation ladder for problems that produce no message. · All modules