Skip to content

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}, a Point, 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 const member function, this has type const Book*. That is precisely why you cannot call a non-const member from it (§7 of Concepts) — the compiler has no way to give you a non-const pointer 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:

void printSummary(const Loan& loan);   // can call only const members

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&) suppresses Book() — so Book 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::string and bool, 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

class Book {
public:
    int id() const { return id_; }
private:
    int id_ = 0;
}                                   // ← no semicolon
error: expected ';' after class definition
    9 | }
      |  ^
      |  ;

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

int copy = book.id_;
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

void brokenB(const Book& book) {
    book.correctPagesTo(500);
}
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:

collect2.exe: error: ld returned 1 exit status

On GCC installations that print the full linker diagnosis, the same failure reads:

undefined reference to `Book::describe() const'
collect2: error: ld returned 1 exit status

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:

error: redefinition of 'class Book'
note: previous definition of 'class Book'

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.

  1. 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).
  2. 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.
  3. Count the setters. For each, ask what would break if it vanished. "Nothing would break" is a normal answer, and the setter should go.
  4. Mark the const members. 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.
  5. 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