Module 2 — Misconceptions and Mistakes¶
Two separate lists, because they need different treatment.
Misconceptions are wrong mental models. They produce code that compiles, looks right, and is wrong — and they are not fixed by the compiler, only by finding out why they are wrong. Mistakes are errors. They produce a message, and the message tells you what to do.
Work through Part 1 properly. Part 2 you can use as a lookup table.
Part 1: Misconceptions¶
M1. "A class is data plus the functions that operate on it"¶
This is nearly right, and the "nearly" causes most of the damage. It suggests a container: a bag of variables with some methods attached, which a caller can still pick apart.
The better model: a class is a set of promises about an object. The data is how those promises happen to be kept today; the member functions are the promises being made.
The difference is testable. Under the container model, this is a reasonable class:
class BankAccount {
public:
long long balance; // public: it is just data, right?
void deposit(long long paisa) { balance += paisa; }
};
Under the promise model, it is not, because there is no promise left to make —
account.balance = -50000000; is legal, and the class cannot claim anything about the
balance it reports. examples/module-02/05-bank-account/main.cpp is the same class
with the promises intact.
Why this matters beyond style: the container model forces you to keep rules in your head and to remember them at every call site. The promise model puts each rule in exactly one place, and makes breaking it a compile error. That is the entire trade this course is about.
M2. "private hides the data"¶
private hides nothing at runtime. There is no encryption, no protection, no
performance cost — the compiled program is byte-for-byte the same as if the members
were public.
What private does is compile-time: it stops other code from naming the member.
That is a much smaller guarantee, and a much more valuable one:
- A caller cannot depend on how you store things, so you may change storage freely.
- The compiler catches the dependency at build time rather than after release.
The corollary is uncomfortable and important: private does not make your class
correct. A private field with a public setter is just as breakable as a public field
— the caller has to write one more character. If you can set every field from outside,
you have hidden the representation (good) and revealed the rules (fatal). That gap
is exactly what Module 4 is for, and
examples/module-04/01-broken-encapsulation/main.cpp drives it home by breaking the
rules of a fully-private class in one line per rule.
M3. "Every data member needs a getter and a setter"¶
The most common structural mistake in student code, and the one that most reliably undoes the point of the module.
Ask the question this course asks instead: what does the caller actually want to do? Not "what would they like to read or write", but what operation of the system are they performing?
| Asked for | Usually wanted | Why it is better |
|---|---|---|
int pages() const |
bool isLongRead() const |
The threshold lives in the class, not in every caller |
void setOnLoan(bool) |
bool lend() |
"Mark it out even though it is out" is not something the system does |
void setBorrowedCount(int) |
bool borrow(title) |
The limit is enforced, not merely documented |
std::vector<Book>& items() |
Book* findById(int) |
A caller cannot reorder or corrupt a collection it was never given |
Getters and setters are not forbidden. A genuinely read-only attribute —
Book::id() — needs a getter and nothing else. The mistake is automatic accessors:
one per field, written before thinking about what the caller needs.
M4. "struct is a value type; class is an object type"¶
A rule imported from other languages. In C++ they are the same construct with two
different defaults (public for struct, private for class; and the same for the
default inheritance mode). A struct may have constructors, destructors, private
members, virtual functions and inheritance. A class may have all-public members.
The convention this course follows:
struct— a plain bundle of data with no rules:Rgb{255, 0, 0}, aPoint, a function's little result package.class— data with rules: keep it private and expose operations.
If you are unsure, ask whether anything must be true about the data. If nothing must
be, a struct is honest and short.
M5. "this is the object"¶
this is a pointer to the object. *this is the object:
Book& withPages(int pages) {
pages_ = pages;
return *this; // the object — so this returns Book&
// return this; // ✗ a Book* where a Book& is required: type error
}
Two consequences that are worth knowing before they surprise you:
this->title_and(*this).title_are the same thing. Nobody writes the second.- Inside a
constmember function,thishas typeconst Book*. That is precisely why you cannot call a non-constmember from it (§7 of Concepts) — the compiler has no way to give you a non-constpointer to an object you promised not to modify.
M6. "A const member function means the object is const"¶
The const is about the function, not the object. It says: calling this will not
change the object.
It is pronounced "const member function", and its real value is on the other side of the call:
const Loan& is how C++ passes objects that must not be modified — cheaply, without a
copy. If your type has no const member functions, this function cannot be written,
and your type cannot be used where it most needs to be: in const contexts, in
containers, and in read-only helper functions.
The mistake to avoid is the opposite reflex: marking a function const and then using
const_cast to modify the object anyway. If you need to modify it, it should not be
const, and the caller's declaration should change. (mutable — Module 4 — is the one
legitimate exception, and it is legitimate only for things that are not part of the
object's observable state.)
M7. "Objects in C++ behave like objects in Java or C#"¶
Java and C# variables of a class type are references; C++ variables of a class type are the objects.
Book a(1001, "C++ Primer");
Book b = a; // ✗ in Java-thinking: b is another name for a
// ✓ in C++: b is a SEPARATE object, copied from a
b.correctPagesTo(500); // `a` is untouched
This one misconception explains a whole category of later confusion — why passing a
class by value copies it, why functions take const Book& instead of Book, and why
Module 10 has to say anything at all about copying. The best summary: in C++, Book b
= a; behaves exactly like int b = a; does for an int.
Preview of a trap this sets. If a class holds a pointer or owns a resource, the compiler's generated copy copies the pointer — two objects now own one resource, and both will free it. In Java that never happens. In C++ it is Module 10, and it is the most dangerous default in the language.
M8. "If I don't write a constructor, I get a sensible one for free"¶
You get one only sometimes, and "sensible" is doing a lot of work in that sentence:
- If you declare any constructor, the compiler generates no default
constructor.
Book(int, const std::string&)suppressesBook()— soBook book;becomes an error. This is the single most common error when a class gains its first constructor, and it is Module 3's opening subject. - The compiler generates a copy constructor that copies each member. For a class
of
int,std::stringandbool, that is exactly right. - For a class holding a raw pointer, that generated copy is often wrong — see M7's preview.
The rule to carry out of this module: the compiler generates the special member functions unless you prevent it, and its version is correct exactly when each member can be copied by value safely.
Part 2: Mistakes, and what they look like¶
Every message below was produced by compiling the code shown, with the course's own command. The text is real.
D1 — the missing semicolon after a class¶
The compiler usually shows you exactly where to type the character. It is worth knowing that this error exists, because it is the one error whose message names the fix.
D2 — reading a private member¶
error: 'int Book::id_' is private within this context
note: declared private here
note: field 'int Book::id_' can be accessed via 'int Book::id() const'
Note the third line: modern GCC often tells you the intended accessor, if one exists.
The fix is not to make id_ public. It is to call id() — or, better, to ask
yourself why the caller needs the identifier at all, and whether the class should be
answering a more useful question (hasId(int)).
D3 — calling a non-const member on a const object¶
error: passing 'const Book' as 'this' argument discards qualifiers [-fpermissive]
note: in call to 'void Book::correctPagesTo(int)'
Read it as the compiler reasoning out loud: "you handed me a const Book; this
function might modify it; this would have to be a const Book* and it isn't." Two
correct responses — mark the function const if it does not modify, or accept a
non-const reference if it does. const_cast is a third response and usually a bug.
D4 — declared but never defined¶
class Book {
public:
void describe() const; // declared...
};
// ...and there is no Book::describe anywhere.
The file compiles, then fails to link:
On GCC installations that print the full linker diagnosis, the same failure reads:
Either way it is a link error: main saw a promise, and nobody kept it. The
deciding evidence is not the wording — it is that compilation succeeded and linking
did not. Fixed in one of three ways: define the function, or add the file that defines
it to the compile command, or stop declaring it. See
Debugging for the routine.
D5 — the class defined twice in one translation unit¶
A header without #pragma once (or an include guard), included twice:
The fix is the first line of the header. If you meet this error and the header does
have #pragma once, check that you have not defined the class in two different
headers, or in a header and a .cpp.
D6 — the constructor parameter that hides the member¶
Book(int id, const std::string& title) {
id = id; // compiles, does nothing
title = title; // compiles, does nothing
}
No error at all, and the object is left uninitialised — the worst outcome in this
list, because nothing tells you. this->id = id; fixes it; naming members id_ (the
course's style) prevents it. Compilers warn about the symptom at higher warning
levels (-Wself-assign), which is yet another reason the course compiles with
-Wall -Wextra and treats warnings as errors.
D7 — the most vexing parse¶
warning: empty parentheses were disambiguated as a function declaration [-Wvexing-parse]
20 | Loan defaultLoan();
Loan defaultLoan(); declares a function returning a Loan, not a Loan. It is
a declaration, so no object exists, and later use produces confusing errors about a
"non-class type". Write Loan defaultLoan{}; — braces cannot be parsed as a function
declaration.
The two-minute self-check for any class you write¶
Run this before you move on from an exercise. It catches M1–M3 and M6, which are the ones the compiler cannot catch for you.
- State the invariant in one sentence, if there is one. If you cannot, either there is none (fine) or you have not decided yet (not fine).
- List the public members. For each: which operation of the system is it? If the honest answer is "it lets the caller read/write a field", do M3's question again.
- Count the setters. For each, ask what would break if it vanished. "Nothing would break" is a normal answer, and the setter should go.
- Mark the
constmembers. Every function that does not modify the object. If you skipped one, the compiler will eventually find it for you in somebody else's function signature. - Can a caller put the object into a state your invariant forbids? If yes, the rule is not encapsulated yet — and Module 4 is where you fix that properly.
Next: Debugging — the technique this module adds, and the escalation ladder for when nothing works. · All modules