Your country

Tools that support it use your country for local currency, number formats, units and paper size. Your choice is saved only in this browser.

Type a name or a two-letter code. Use the up and down arrow keys to move through the countries, Enter to choose one and Escape to close.

C++  Module 7 – Classes, objects and RAII

C++ classes, member functions and access control

Write C++ classes that keep their data private, protect an invariant through member functions, and use const so read-only objects stay read-only.

  • Beginner
  • 20 minutes
  • Examples run with Clang 21.0.0, GCC 16.2.0 and Python 3.14.8
  • By MySmartCoPilot
Free lesson, Pro answers: 20 minutes
Free: the whole lesson. With a pass: the model answers to its 2 interview questions.

What you will learn

  • Define classes with private data and public member functions
  • Protect an invariant through the class interface
  • Write const-correct member functions
  • Decide whether a function belongs inside the class or outside it

Before you start

One line anywhere in a program, wallet.paise = -500;, is enough to leave a struct holding a debt that no rule allows, and nothing points to the line that did it. A class closes that hole. Its data members are private, so only its own member function: A function declared inside a class and called on one object of it with a dot, as in asha.deposit(500). It may use the private members of every object of its class. can change them, and a few public member functions form its interface, the only way the rest of the program can use an object. When each of those functions checks its argument before it changes anything, a rule such as “the balance is never negative” holds for every object at every moment: an invariant: A rule about an object's data that holds whenever none of its member functions is running, such as "the balance is never negative". A class keeps it by making the data private and checking every change in its member functions. you can prove from the class alone.

Below, a bank account that keeps its money in paise is built with Apple clang 21 and GCC 16. You will step through its calls, see both compilers refuse code that reaches past the interface, mark the functions that only read as const, and decide which functions belong inside the class at all.

A class lets only its own functions change its data

A struct with a public member lets any code write any value into it: with struct Wallet { long long paise; };, the assignment of -500 compiles in every file that can see a Wallet. Here is the same account as a class:

bank_account.cpp C++ · bank_account.cpp
// The balance is private, so the only way to change it is through member functions
// that keep the class's rule: the balance is never negative.
#include <format>
#include <print>
#include <string>

class BankAccount {
public:
    // Adds money; refuses an amount that is zero or negative.
    bool deposit(long long paise) {
        if (paise <= 0) return false;
        balance_ += paise;
        return true;
    }

    // Takes money out; refuses an amount that would leave the balance below zero.
    bool withdraw(long long paise) {
        if (paise <= 0 || paise > balance_) return false;
        balance_ -= paise;
        return true;
    }

    // Moves money into another account.
    bool transfer_to(BankAccount& other, long long paise) {
        if (!withdraw(paise)) return false;
        other.balance_ += paise;   // allowed: access is granted per class, not per object
        return true;
    }

    long long balance() const { return balance_; }   // const: it only reads

private:
    long long balance_ = 0;   // paise (1 rupee = 100 paise); never below 0
};

// A free function: it needs nothing private, so it is not a member.
std::string in_rupees(long long paise) {
    return std::format("{}.{:02}", paise / 100, paise % 100);
}

int main() {
    BankAccount asha;
    BankAccount dev;
    std::println("deposit 50000:  {}", asha.deposit(50'000));
    std::println("withdraw 80000: {}", asha.withdraw(80'000));
    std::println("deposit -100:   {}", asha.deposit(-100));
    std::println("transfer 12550: {}", asha.transfer_to(dev, 12'550));
    std::println("asha {} rupees, dev {} rupees", in_rupees(asha.balance()), in_rupees(dev.balance()));
    std::println("sizeof(BankAccount) = {}, sizeof(long long) = {}", sizeof(BankAccount), sizeof(long long));
}
Built and run with both compilers Python · bank_account_session.py
from session import run

# bank_account.cpp built and run with Apple clang, then with GCC 16.
run("""
clang++ -std=c++23 -Wall -Wextra bank_account.cpp -o bank_account && ./bank_account
g++-16 -std=c++23 -Wall -Wextra bank_account.cpp -o bank_account-gcc && ./bank_account-gcc
""")

Output

$ clang++ -std=c++23 -Wall -Wextra bank_account.cpp -o bank_account && ./bank_account
deposit 50000:  true
withdraw 80000: false
deposit -100:   false
transfer 12550: true
asha 374.50 rupees, dev 125.50 rupees
sizeof(BankAccount) = 8, sizeof(long long) = 8
$ g++-16 -std=c++23 -Wall -Wextra bank_account.cpp -o bank_account-gcc && ./bank_account-gcc
deposit 50000:  true
withdraw 80000: false
deposit -100:   false
transfer 12550: true
asha 374.50 rupees, dev 125.50 rupees
sizeof(BankAccount) = 8, sizeof(long long) = 8

Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 bank_account_session.py

Every change to balance_ goes through a member function, and each one checks its argument before it touches the data. Step through the four calls that main makes and watch each account’s balance after every one:

Interactive · message by message Four calls on two accounts, and the rule after each Step through the calls that main makes. Each account's line shows its balance_ in paise, the data no code outside the class can touch.
Four calls on two accounts, and the rule after each, step 7 of 7. Messages so far: main to asha: deposit(50'000); main to asha: withdraw(80'000); main to asha: deposit(-100); main to asha: transfer_to(dev, 12'550); asha to dev: other.balance_ += 12'550.maincalls public functionsashabalance_ 37,450devbalance_ 12,550deposit(50'000)withdraw(80'000)deposit(-100)transfer_to(dev, 12'550)other.balance_ += 12'550

The drawing shows the whole scene; its 7 steps are listed below.

Every step as text
  1. Two valid accounts. BankAccount asha; and BankAccount dev; create two accounts. The default member initialiser = 0 gives each balance_ the value 0, so both obey the rule from their first moment.
  2. deposit(50'000). asha.deposit(50'000) checks that the amount is above 0, then adds it. The call returns true, the first line of the output, and balance_ is 50,000 paise.
  3. withdraw(80'000). withdraw tests 80,000 > balance_ before it subtracts anything. The test is true, so the call returns false and balance_ stays 50,000: a refused call changes nothing.
  4. deposit(-100). -100 is not a deposit, so deposit returns false at its first line. Again nothing changed, and the balance still obeys the rule.
  5. The transfer's withdrawal. asha.transfer_to(dev, 12'550) first calls withdraw(12'550) on asha. That passes its check and leaves 37,450 paise.
  6. Into another object. Then it adds the amount to other.balance_, the private member of dev. That compiles, because access is granted to the code of BankAccount, not to one object.
  7. The rule held throughout. main prints asha 374.50 rupees, dev 125.50 rupees. After every call both balances were 0 or more, and no line outside the class could have written them.

The withdrawal of 80,000 paise is refused because the account holds only 50,000, and the deposit of -100 is refused because it is not a deposit at all. Neither refusal changed anything, so the account still satisfies its rule after every call, and you can read that off the class definition alone: nothing outside it can assign to balance_.

The last line of the output is worth a second look. A BankAccount is 8 bytes, exactly the size of the long long inside it. Access control is a compile-time check of names. The object holds the same bytes as a struct with the same member would, and calling asha.balance() costs the same as reading the member directly once the compiler inlines the call.

Keep money in whole paise

Store the smallest unit as an integer. A double cannot hold most decimal fractions exactly: 0.1 is stored as the nearest binary fraction, and sums of such values drift. Whole paise, the hundredths of a rupee, in a long long are exact, and the same fix works in any currency.

Checkpoint

Check: Private data behind public functions

  1. Question 1 of 2 asha holds 50,000 paise, and asha.withdraw(80'000) returns false. What does asha.balance() return now?

    Choose one answer.

    A hint

    Look at the order of the two lines inside withdraw.

    Show the answer: Question 1 of 2

    Answer: 50,000

    withdraw returns false at its first line when the amount is larger than the balance, before the line that subtracts. The account still holds 50,000 paise and still keeps its rule.

  2. Question 2 of 2 The program printed sizeof(BankAccount). How many bytes is a BankAccount, whose only data member is a long long?

    Type a number.

    A hint

    Access control is checked by the compiler. Does it add anything to the object?

    Show the answer: Question 2 of 2

    Answer: 8 bytes

    The last line of the output says 8, the size of the long long inside. private and public are checked while the program compiles; the object holds the same bytes as a struct with the same member would.

Access is checked by class, while compiling

The access specifier: One of the labels public:, private: and protected: in a class. Each applies to the members declared after it, up to the next label; members of a class start out private and those of a struct public. public: and private: apply to every member declared after them, until the next label. A class may use each label as often as it likes and in any order; the common style puts the public interface first, because that is what someone reading the class needs, and the private details last. A third label, protected:, matters only for inheritance, which the next module covers.

Without any label, the members of a class are private and those of a struct are public, and base classes follow the same default. That default is the only difference between the two keywords. Rule C.2 of the C++ Core Guidelines builds the convention on it: use class when the members are tied together by an invariant, and struct when each member can take any value on its own.

Who can use a public or a private member
Criterion publicprivate
Own member functions YesYes
Same, other object YesYes
Any other code YesNo
Default in a class NoYes
Default in a struct YesNo

A “No” in the third row is a compile error, as the next example shows. The second row, a member function working on another object of the same class, explains the last call of the figure above: transfer_to adds to other.balance_, the private member of a different account, and that compiles. Access is granted to the code of a class, not to one object: any member function of BankAccount may use the private members of any BankAccount.

Code outside the class cannot. This file tries to set the balance from main:

private_access.cpp C++ · private_access.cpp
// Code outside the class tries to set the balance directly.
class BankAccount {
public:
    long long balance() const { return balance_; }

private:
    long long balance_ = 0;
};

int main() {
    BankAccount acct;
    acct.balance_ = -500;   // not allowed: balance_ is private
    return acct.balance() < 0;
}
Both compilers refuse the assignment Python · private_access_session.py
from session import run

# Both compilers reject the assignment to a private member.
run("""
clang++ -std=c++23 -Wall -Wextra -c private_access.cpp
g++-16 -std=c++23 -Wall -Wextra -c private_access.cpp
""")

Output

$ clang++ -std=c++23 -Wall -Wextra -c private_access.cpp
private_access.cpp:12:10: error: 'balance_' is a private member of 'BankAccount'
   12 |     acct.balance_ = -500;   // not allowed: balance_ is private
      |          ^
private_access.cpp:7:15: note: declared private here
    7 |     long long balance_ = 0;
      |               ^
1 error generated.
[exit status 1]
$ g++-16 -std=c++23 -Wall -Wextra -c private_access.cpp
private_access.cpp: In function 'int main()':
private_access.cpp:12:10: error: 'long long int BankAccount::balance_' is private within this context
   12 |     acct.balance_ = -500;   // not allowed: balance_ is private
      |          ^~~~~~~~
private_access.cpp:7:15: note: declared private here
    7 |     long long balance_ = 0;
      |               ^~~~~~~~
private_access.cpp:12:10: note: field 'long long int BankAccount::balance_' can be accessed via 'long long int BankAccount::balance() const'
   12 |     acct.balance_ = -500;   // not allowed: balance_ is private
      |          ^~~~~~~~
      |          balance()
[exit status 1]

Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 private_access_session.py

Both errors name the member and point to the line where it was declared private. Clang says it “is a private member”; GCC says it “is private within this context” and, in GCC 16, adds a hint that the field can be read through the public balance() const function. Both compilers accept a switch, -fno-access-control, that turns the check off; GCC’s manual describes it as a way to work around bugs in the compiler’s own access checking, so it is no way into a class. To let main write the balance, you would have to change the class, and that is the protection.

Checkpoint

Check: Access is per class and checked while compiling

  1. Question 1 of 2 What is the only difference between the keywords struct and class?

    Choose one answer.

    Show the answer: Question 1 of 2

    Answer: The default access, for members and base classes, until a label says otherwise

    struct and class declare the same kind of type. The default access is the one difference, which is why rule C.2 of the C++ Core Guidelines uses class for types with an invariant and struct for plain data.

  2. Question 2 of 2 Does this member function compile?

    Read the code, then choose one answer.

    class Account {
    public:
        bool same_balance(const Account& other) const {
            return balance_ == other.balance_;
        }
    private:
        long long balance_ = 0;
    };
    Show the answer: Question 2 of 2

    Answer: Yes, because access is granted per class, so it may read another Account's private member

    Access control is checked by class, not by object: any member function of Account may use the private members of every Account. That is how transfer_to adds to other.balance_ in the lesson.

A member function runs on the object before the dot

A call such as asha.deposit(50'000) runs deposit for one particular object. Inside the function, the name balance_ refers to that object’s member. The compiler makes this work by passing the function a hidden pointer to the object, which the body can name as this: Inside an ordinary member function, a pointer to the object the function was called on: in a.add(4'000), this is &a. A member name such as total_ in the body means this->total_.. Writing this->balance_ and writing balance_ mean the same thing.

this_pointer.cpp C++ · this_pointer.cpp
// Inside a member function, this points to the object the function was called on.
#include <print>

class Basket {
public:
    Basket& add(long long price_paise);   // declared here, defined after the class
    long long total() const { return total_; }
    int items() const { return items_; }
    bool is_object_at(const Basket* where) const { return this == where; }

private:
    long long total_ = 0;
    int items_ = 0;
};

// Basket:: names the class this member function belongs to.
Basket& Basket::add(long long price_paise) {
    this->total_ += price_paise;   // the same as total_ += price_paise
    ++items_;
    return *this;                  // the object itself, so calls can be chained
}

int main() {
    Basket a;
    Basket b;
    a.add(4'000).add(2'550).add(999);
    std::println("a: {} items, {} paise", a.items(), a.total());
    std::println("b: {} items, {} paise", b.items(), b.total());
    std::println("a.is_object_at(&a): {}", a.is_object_at(&a));
    std::println("b.is_object_at(&a): {}", b.is_object_at(&a));
}
this points to the object the function was called on Python · this_pointer_session.py
from session import run

# this_pointer.cpp built and run with Apple clang, then with GCC 16.
run("""
clang++ -std=c++23 -Wall -Wextra this_pointer.cpp -o this_pointer && ./this_pointer
g++-16 -std=c++23 -Wall -Wextra this_pointer.cpp -o this_pointer-gcc && ./this_pointer-gcc
""")

Output

$ clang++ -std=c++23 -Wall -Wextra this_pointer.cpp -o this_pointer && ./this_pointer
a: 3 items, 7549 paise
b: 0 items, 0 paise
a.is_object_at(&a): true
b.is_object_at(&a): false
$ g++-16 -std=c++23 -Wall -Wextra this_pointer.cpp -o this_pointer-gcc && ./this_pointer-gcc
a: 3 items, 7549 paise
b: 0 items, 0 paise
a.is_object_at(&a): true
b.is_object_at(&a): false

Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 this_pointer_session.py

Step through the program and watch where this points in each call: first along the chain of calls on a, then in the two calls of is_object_at:

Interactive · step by step What this points to in each call of this_pointer.cpp Step through the chained calls on a, then the two calls of is_object_at. The left box is this; the arrow shows which Basket it points to.
What this points to in each call of this_pointer.cpp, step 6 of 6. Showing this (&b), Basket a (3 items, 7,549 paise), Basket b. In focus: this, Basket b.points tothis&bBasket a3 items, 7,549 paiseBasket b0 items, 0 paise

The drawing shows the whole scene; its 6 steps are listed below.

Every step as text
  1. a.add(4'000). a.add(4'000) runs Basket::add with this set to &a, the address of the object before the dot. total_ += price_paise in the body means this->total_, so a holds 1 item and 4,000 paise.
  2. return *this. add ends with return *this;, which hands back a itself, by reference, not a copy. The next call of the chain therefore runs on a again.
  3. .add(2'550). The second call gets the same this, &a: total_ becomes 6,550 paise and items_ 2.
  4. .add(999). The third call adds 999: a holds 3 items and 7,549 paise, the first line of the output. b was never before a dot, so it still holds 0 items, the second line.
  5. a.is_object_at(&a). is_object_at compares this with the address it is given. Called on a with &a, this is &a, so the comparison is true, as the third line shows.
  6. b.is_object_at(&a). Called on b, this is &b. The argument is still &a, so the comparison is false, the last line: this always names the object before the dot, never an argument.

Three details of this file come up in every larger class:

  • add is declared inside the class and defined after it. The definition names the class, Basket::add, so the compiler knows which class the function belongs to and lets the body use the private members.
  • return *this; returns the object itself, by reference, so the calls can be chained: a.add(4'000).add(2'550).add(999) adds three prices to the same basket.
  • is_object_at compares this with an address and shows that this is the address of the object before the dot: a.is_object_at(&a) is true and b.is_object_at(&a) is false.

A member function defined inside the class body is implicitly inline (in code that does not use C++20 modules), so a header can contain the whole class and be included by many .cpp files without the duplicate symbol error of the lesson on compiling. A definition written after the class, as Basket::add is here, goes in a .cpp file, or is marked inline when it stays in a header.

Checkpoint

Check: this and the object before the dot

  1. Question 1 of 2 Inside the call b.is_object_at(&a), what does this hold?

    Choose one answer.

    Show the answer: Question 1 of 2

    Answer: &b, the address of the object before the dot

    The call passes &b as the hidden this and &a as where. Comparing the two gives false, the last line the program printed.

  2. Question 2 of 2 add is changed to return a Basket by value, with the same body. What do a.items() and a.total() return after the chain?

    Read the code, then choose one answer.

    Basket add(long long price_paise) {   // returns a copy now
        total_ += price_paise;
        ++items_;
        return *this;
    }
    // in main
    Basket a;
    a.add(4'000).add(2'550).add(999);
    A hint

    Which object does the second call of the chain run on?

    Show the answer: Question 2 of 2

    Answer: 1 item and 4,000 paise

    return *this; copies a when the return type is Basket. The second add is then called on that temporary copy and the third on another one, so a keeps 1 item and 4,000 paise. The reference in the lesson's Basket& is what makes chaining work.

A const member function promises not to change the object

balance() in bank_account.cpp is declared long long balance() const, a const member function: A member function with const after its parameter list, as in long long balance() const. It may not change the object, and it is the only kind of member function a const object or a const reference can call.. The const after the parameter list is a promise that the function does not change the object. Inside it, this points to a const object, so an assignment such as balance_ = 0; in its body is a compile error.

The promise matters to the caller. A function that receives const BankAccount& acct may only call the member functions of acct that are marked const, and so may a const BankAccount variable:

Which member functions each way of reaching an account can call
Criterion const member functionAny other
BankAccount acct YesYes
const BankAccount& ref YesNo: compile error
const BankAccount acct YesNo: compile error

Forget the marker on a function that only reads, and a correct-looking call in the second row fails:

const_member.cpp C++ · const_member.cpp
// print_statement promises not to change the account (it takes a const reference),
// then calls a member function that was not marked const.
#include <print>

class BankAccount {
public:
    bool deposit(long long paise) {
        if (paise <= 0) return false;
        balance_ += paise;
        return true;
    }
    long long balance() { return balance_; }   // missing: const after ()

private:
    long long balance_ = 0;
};

void print_statement(const BankAccount& acct) {
    std::println("balance: {} paise", acct.balance());
}

int main() {
    BankAccount acct;
    acct.deposit(2'500);
    print_statement(acct);
}
A non-const member function called through a const reference Python · const_member_session.py
from session import run

# A non-const member function called through a const reference.
run("""
clang++ -std=c++23 -Wall -Wextra -c const_member.cpp
g++-16 -std=c++23 -Wall -Wextra -c const_member.cpp
""")

Output

$ clang++ -std=c++23 -Wall -Wextra -c const_member.cpp
const_member.cpp:19:39: error: 'this' argument to member function 'balance' has type 'const BankAccount', but function is not marked const
   19 |     std::println("balance: {} paise", acct.balance());
      |                                       ^~~~
const_member.cpp:12:15: note: 'balance' declared here
   12 |     long long balance() { return balance_; }   // missing: const after ()
      |               ^
1 error generated.
[exit status 1]
$ g++-16 -std=c++23 -Wall -Wextra -c const_member.cpp
const_member.cpp: In function 'void print_statement(const BankAccount&)':
const_member.cpp:19:51: error: passing 'const BankAccount' as 'this' argument discards qualifiers [-fpermissive]
   19 |     std::println("balance: {} paise", acct.balance());
      |                                       ~~~~~~~~~~~~^~
const_member.cpp:12:15: note:   in call to 'long long int BankAccount::balance()'
   12 |     long long balance() { return balance_; }   // missing: const after ()
      |               ^~~~~~~
[exit status 1]

Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 const_member_session.py

Clang states the problem directly: the object has type const BankAccount, “but function is not marked const”. GCC’s message, “passing ‘const BankAccount’ as ‘this’ argument discards qualifiers”, says the same in older terms: calling balance() would turn a pointer to a const object into a plain pointer. The [-fpermissive] at the end of GCC’s line names an option that would downgrade this error to a warning. GCC’s manual describes it as a way to let some nonconforming code compile, and here it would only hide a real mistake.

Removing const from the caller

The error points at the call, so it is tempting to change print_statement(const BankAccount& acct) into print_statement(BankAccount& acct). That compiles, but the function no longer promises to leave the account alone, and callers that hold a const account can no longer use it. Fix the class instead: long long balance() const.

Rule Con.2 of the C++ Core Guidelines turns this into a habit: every member function is const unless calling it changes what the object looks like from outside. Readers then see at a glance which calls can modify an object, and const references can be passed around freely.

Checkpoint

Check: const member functions

  1. Question 1 of 2 Which call fails to compile?

    Read the code, then choose one answer.

    class Account {
    public:
        bool withdraw(long long paise);
        long long balance() const;
    };
    
    void report(const Account& a) {
        a.balance();       // (1)
        a.withdraw(100);   // (2)
    }
    Show the answer: Question 1 of 2

    Answer: Only (2)

    Through a const reference you can call only const member functions. (2) is the error Clang reports as "function is not marked const" and GCC as "discards qualifiers".

  2. Question 2 of 2 print_statement(const BankAccount& acct) calls acct.balance(), and balance() is not marked const. What is the right fix?

    Choose one answer.

    Show the answer: Question 2 of 2

    Answer: Add const after the parameter list of balance(): long long balance() const

    The error points at the call, but the mistake is in the class: a function that only reads was not declared const. Rule Con.2 of the C++ Core Guidelines makes const the default for such functions.

Operations, not setters, keep the invariant

Outside code calls the public functions, which guard the private balance; writing the balance directly is a compile error.Code outside the classmain() and other functionsPublic interfacedeposit() withdraw()transfer_to() balance() constPrivate databalance_ in paiserule: never below 0callschecks the argument,then changes the dataacct.balance_ = -500;compile error

The public interface is the only way to the private data

Text description of the diagram

The diagram has three boxes: code outside the class (main and other functions), the public interface of BankAccount (deposit, withdraw, transfer_to and balance) and the private data (balance_ in paise, with the rule that it is never below 0).

  1. Code outside the class calls the public member functions.
  2. Each public function checks its argument first and changes the private balance only when the rule still holds afterwards.
  3. A dashed arrow from the outside code straight to the private data is labelled "acct.balance_ = -500; compile error": the compiler refuses any direct access, so the only way to the data is through the interface.

The diagram shows the shape every class in this module has: the rest of the program reaches the data only through the public functions, and each of those functions keeps the rule. Three habits keep that shape intact:

  • Offer operations, not setters. deposit and withdraw describe what users of an account do. A public set_balance(long long) would accept -500 just as the struct did, and would reopen the hole the class closed. Rule C.131 of the Core Guidelines warns against getters and setters that do nothing but read or write a member.
  • Check first, then change. withdraw tests its argument before it subtracts, so a refused call leaves the object exactly as it was. A function that changed one member and then gave up halfway would leave the invariant broken.
  • Start valid. The default member initialiser = 0 makes every new account valid from its first moment. The next lesson adds constructors, for classes whose objects need values from the caller before they can be valid.

When the members have no rule between them, as in a point with two coordinates, a struct with public members is the simpler and better choice.

Key idea

A class keeps its invariant when its data is private, every public function checks before it changes anything, and no function hands out a way to write the data unchecked.

Checkpoint

Check: An interface that keeps the invariant

  1. Question 1 of 2 Which addition to BankAccount would let a negative balance happen again?

    Choose one answer.

    Show the answer: Question 1 of 2

    Answer: A public void set_balance(long long paise)

    The class keeps its rule only while every public way to change the data checks it. A trivial setter, which rule C.131 of the C++ Core Guidelines warns against, checks nothing.

  2. Question 2 of 2 A member move_to(BankAccount& other, long long paise) first adds paise to other.balance_, then calls withdraw(paise), which refuses. What is wrong now?

    Choose one answer.

    A hint

    Add the two balances before and after the call.

    Show the answer: Question 2 of 2

    Answer: other gained money that this account never gave, so the two balances add up to more than before

    Check first, then change: transfer_to in the lesson calls withdraw first and adds to the other account only when it succeeded. Changing one part and then giving up halfway leaves the objects in a state no rule allows.

A function that needs no private data stays outside the class

Not every function that works with an object should be a member. Rule C.4 of the Core Guidelines keeps the member list for functions that cannot do their job without touching the private data. Sorting the functions of bank_account.cpp, and a transfer built from the interface, by that test:

Where each BankAccount function belongs (rule C.4)
Criterion Uses balance_?Belongs
deposit Writes itMember
withdraw Writes itMember
balance Reads itMember, const
in_rupees No, only its argumentFree function
transfer, from withdraw and deposit No, only the interfaceFree function

in_rupees turns any number of paise into text such as 374.50; it needs nothing private, so it is an ordinary function outside the class. Every member function is a place where the invariant can be broken, so a class with fewer members is easier to check.

transfer_to shows the same choice. As a member it may add to other.balance_ directly, which is how this lesson demonstrated per-class access. It could just as well be a free function built from the public interface, bool transfer(BankAccount& from, BankAccount& to, long long paise), which calls from.withdraw(paise) and then to.deposit(paise). That version cannot break the rule at all, because the two functions it uses already keep it.

Checkpoint

Check: Member or free function

  1. Question 1 of 2 Following rule C.4 of the C++ Core Guidelines, which of these functions for BankAccount should be members?

    Choose every answer that is right.

    Show the answer: Question 1 of 2

    Answer:

    • withdraw, which checks the balance before it subtracts from the private member
    • deposit, which adds to the private member after checking the amount

    A function should be a member only if it needs direct access to the private data. withdraw and deposit change balance_ directly; in_rupees and a transfer built from withdraw and deposit use only the public interface, so they can live outside the class, where they cannot break the invariant.

  2. Question 2 of 2 bool transfer(BankAccount& from, BankAccount& to, long long paise) is a free function that returns false when from.withdraw(paise) fails and otherwise calls to.deposit(paise). Why can it never leave a balance negative?

    Choose one answer.

    Show the answer: Question 2 of 2

    Answer: It can only call public functions, and each of them already keeps the rule

    A function built only from the public interface inherits every check of that interface. That is why rule C.4 keeps such functions out of the class: they cannot break the invariant even if they are wrong.

Practise: a race clock that keeps its invariant

RaceTime holds a time on a race clock, such as 3:05:09 for three hours, five minutes and nine seconds. Its invariant is that minutes() and seconds() are always between 0 and 59. The starter code does not keep it yet:

The starter code and its sample tests

C header · race_time.h

// Starter code for "Your turn": make RaceTime keep its invariant.
#pragma once
#include <compare>
#include <format>
#include <string>

// A time on a race clock, such as 3:05:09 (hours, minutes, seconds).
// The invariant: minutes() and seconds() are always between 0 and 59.
class RaceTime {
public:
    // Adds a split time. Carries every 60 seconds into a minute and every 60 minutes
    // into an hour. Returns false, and changes nothing, if any part is negative.
    bool add(int hours, int minutes, int seconds) {
        // TODO: check the arguments and keep the invariant.
        hours_ += hours;
        minutes_ += minutes;
        seconds_ += seconds;
        return true;
    }

    int hours() const { return hours_; }
    int minutes() const { return minutes_; }
    int seconds() const { return seconds_; }

    // "3:05:09": minutes and seconds always take two digits.
    std::string text() const {
        // TODO: pad minutes and seconds to two digits.
        return std::format("{}:{}:{}", hours_, minutes_, seconds_);
    }

    // Compares hours, then minutes, then seconds: right only while the invariant holds.
    auto operator<=>(const RaceTime&) const = default;

private:
    int hours_ = 0;
    int minutes_ = 0;
    int seconds_ = 0;
};

C++ · race_time_tests.cpp

// Sample tests for race_time.h. Build: clang++ -std=c++23 -Wall -Wextra race_time_tests.cpp -o race_time_tests
#include <print>
#include "race_time.h"

int passed = 0;
int failed = 0;

void expect(bool ok, const char* what) {
    if (ok) {
        ++passed;
    } else {
        ++failed;
        std::println("FAIL {}", what);
    }
}

RaceTime make(int h, int m, int s) {
    RaceTime t;
    t.add(h, m, s);
    return t;
}

int main() {
    expect(RaceTime{}.text() == "0:00:00", "a new RaceTime reads 0:00:00");
    expect(make(0, 0, 75).text() == "0:01:15", "75 seconds carry into 0:01:15");
    expect(make(0, 125, 0).text() == "2:05:00", "125 minutes carry into 2:05:00");

    RaceTime lap = make(1, 59, 59);
    lap.add(0, 0, 1);
    expect(lap.text() == "2:00:00", "one more second after 1:59:59 makes 2:00:00");

    RaceTime run = make(3, 5, 9);
    expect(!run.add(0, -1, 0), "add refuses a negative part");
    expect(run.text() == "3:05:09", "a refused add changes nothing");

    expect(make(0, 90, 0) == make(1, 30, 0), "90 minutes equal 1:30:00");
    expect(make(2, 58, 119) < make(3, 0, 0), "2:59:59 is faster than 3:00:00");
    expect(make(0, 0, 86'400).hours() == 24, "a whole day of seconds is 24 hours");

    std::println("{} of {} checks passed", passed, passed + failed);
    return failed == 0 ? 0 : 1;
}

Before you change anything, the starter code fails most of the checks:

The sample tests against the starter code Python · race_time_session.py
from session import run

# The sample tests against the starter code, before the TODOs are done.
run("""
clang++ -std=c++23 -Wall -Wextra race_time_tests.cpp -o race_time_tests && ./race_time_tests
g++-16 -std=c++23 -Wall -Wextra race_time_tests.cpp -o race_time_tests-gcc && ./race_time_tests-gcc
""")

Output

$ clang++ -std=c++23 -Wall -Wextra race_time_tests.cpp -o race_time_tests && ./race_time_tests
FAIL a new RaceTime reads 0:00:00
FAIL 75 seconds carry into 0:01:15
FAIL 125 minutes carry into 2:05:00
FAIL one more second after 1:59:59 makes 2:00:00
FAIL add refuses a negative part
FAIL a refused add changes nothing
FAIL 90 minutes equal 1:30:00
FAIL a whole day of seconds is 24 hours
1 of 9 checks passed
[exit status 1]
$ g++-16 -std=c++23 -Wall -Wextra race_time_tests.cpp -o race_time_tests-gcc && ./race_time_tests-gcc
FAIL a new RaceTime reads 0:00:00
FAIL 75 seconds carry into 0:01:15
FAIL 125 minutes carry into 2:05:00
FAIL one more second after 1:59:59 makes 2:00:00
FAIL add refuses a negative part
FAIL a refused add changes nothing
FAIL 90 minutes equal 1:30:00
FAIL a whole day of seconds is 24 hours
1 of 9 checks passed
[exit status 1]

Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 race_time_session.py

Edit race_time.h until both compilers print 9 of 9 checks passed:

  1. add refuses a negative part: it returns false and changes nothing.
  2. add carries every 60 seconds into a minute and every 60 minutes into an hour. Converting the stored time and the split to a number of seconds, adding them, and converting back is the simplest way. Do the arithmetic in long long, so that a large split cannot overflow an int.
  3. text pads minutes and seconds to two digits: std::format("{:02}", 5) gives 05.

The class compares times with the defaulted operator<=> from the lesson on structs, member by member: hours, then minutes, then seconds. That comparison is right only while the invariant holds. Without the carry, 90 minutes would be stored as 0 hours and 90 minutes, and it would compare as smaller than 1:30:00 because its hours are smaller. Keeping the invariant inside add is what makes the one-line comparison correct.

Time Calculator Add split times such as 1:59:59 and 0:00:01 to check what your RaceTime should print.

Then try to break the class from the outside: add t.seconds_ = 75; to the tests and build them again. The compiler stops you, which is the protection this lesson is about.

Then test what stayed with you, from the private balance to the free transfer. The session opens on whichever of the six ideas your answers have been weakest on so far, and each choice says what it gets right or misses:

Practice that adapts to you

Practise this lesson

13 questions about 6 ideas of this lesson. Each answer is explained at once, and what you get right is kept in this browser.

  1. Question 1 of 13 Why does BankAccount keep balance_ private?

    Choose one answer.

    Show the answer: Question 1 of 13

    Answer: So that only its member functions, which check every change, can write it

    Private data plus checked member functions put every change of the data in one place, the class. That is what lets the class keep its invariant.

  2. Question 2 of 13 A program uses struct Wallet { long long paise; }; in 200 source files, and one wallet ended up at -500. Where can the line that wrote -500 be?

    Choose one answer.

    Show the answer: Question 2 of 13

    Answer: In any of the 200 files that can see a Wallet

    With public data the search for a bad write covers the whole program. As a class with private data and checked member functions, the only lines that can write the balance are those few functions.

  3. Question 3 of 13 A class is declared as class Meter { int reading; };. Who can read reading?

    Choose one answer.

    Show the answer: Question 3 of 13

    Answer: Only member functions of Meter

    Members of a class declared with the keyword class are private until a public: label, so only Meter's own member functions can use reading. With struct, the same member would be public.

  4. Question 4 of 13 main contains acct.balance_ = -500;, and balance_ is a private member of BankAccount. What happens?

    Choose one answer.

    Show the answer: Question 4 of 13

    Answer: The build fails, with an error that points at the line and at the private declaration

    Access is a compile-time check of names. private_access.cpp did not build with either compiler, so there is no program in which balance_ can be set from main.

  5. Question 5 of 13 Inside a member function called as cart.add(500), what is this?

    Choose one answer.

    Show the answer: Question 5 of 13

    Answer: A pointer to cart

    Every non-static member function receives a pointer to the object it was called on, named this. Writing this->total_ and total_ inside the body means the same member of cart.

  6. Question 6 of 13 With the lesson's Basket, whose add returns *this by reference, how many paise does a.total() return after Basket a; a.add(1'000).add(250);?

    Type a number.

    A hint

    Which object does the second call run on?

    Show the answer: Question 6 of 13

    Answer: 1250 paise

    The first add changes a and returns a itself by reference, so the second add runs on a too: 1,000 + 250 = 1,250 paise.

  7. Question 7 of 13 A member function is declared in the class as Basket& add(long long price_paise); and defined after the class without Basket::, with the lesson's body. What happens?

    Choose one answer.

    Show the answer: Question 7 of 13

    Answer: The definition is an ordinary function named add, and its uses of this do not compile

    Basket::add tells the compiler which class the function is a member of, and only then may the body use this and the private members.

  8. Question 8 of 13 Inside long long balance() const, what is the type of this?

    Choose one answer.

    Show the answer: Question 8 of 13

    Answer: const BankAccount*, a pointer to a const object

    In a const member function this points to a const object. That is why balance_ = 0; in its body is a compile error, and why such a function can be called on const objects.

  9. Question 9 of 13 Rule Con.2 says to make member functions const by default. Which of these BankAccount functions should be const?

    Choose every answer that is right.

    Show the answer: Question 9 of 13

    Answer:

    • long long balance(), which returns balance_
    • bool can_withdraw(long long paise), which compares paise with balance_

    A member function is const unless calling it changes what the object looks like from outside. Readers then see at a glance which calls can change an account.

  10. Question 10 of 13 What makes a new BankAccount satisfy its rule from its first moment?

    Choose one answer.

    Show the answer: Question 10 of 13

    Answer: The default member initialiser = 0 on balance_

    Start valid, check first and offer operations instead of setters: with = 0 every account is valid before the first call. The next lesson adds constructors for classes whose objects need values from the caller.

  11. Question 11 of 13 A Point has two doubles, x and y, and any pair of values is a valid point. What fits it best?

    Choose one answer.

    Show the answer: Question 11 of 13

    Answer: A struct with public members x and y

    Rule C.2 of the C++ Core Guidelines: use class when the members are tied together by an invariant, and struct when each member can take any value on its own.

  12. Question 12 of 13 in_rupees(long long paise) turns 37450 into "374.50". Why is it not a member of BankAccount?

    Choose one answer.

    Show the answer: Question 12 of 13

    Answer: It needs nothing private, and rule C.4 keeps members for functions that need the representation

    Every member function is a place where the invariant can break. A function that needs only its argument stays outside the class, where it cannot.

  13. Question 13 of 13 Why is a class with fewer member functions easier to check?

    Choose one answer.

    Show the answer: Question 13 of 13

    Answer: Each member function can write the private data, so fewer members leave fewer places where the rule can break

    The invariant is safe only if every member function keeps it. Keeping functions that need no private data outside the class makes that list short.

Interview questions

Fresher · What does const after a member function’s parameter list mean?

It promises that the function does not change the object it is called on. Inside such a function, this points to a const object, so the compiler rejects any assignment to a data member, and any call to a member function that is not itself const. The promise is what lets the function be called on const objects and through const references: a function that takes const Account& can call balance() const but not withdraw(). Getting it wrong shows up at the call site, as Clang’s “function is not marked const” or GCC’s “discards qualifiers”. A good answer adds that const is part of the function’s signature, so a class may overload a member function on it, and that a member declared mutable may still change inside a const function, which is meant for caches and counters that do not change what the object represents.

Two follow-ups usually come next: which of two overloads that differ only in const a call picks, and how a const function can still count its own calls. Decide both:

Checkpoint

Interview round: two follow-up questions

  1. Question 1 of 2 Follow-up: a class has two member functions named at. Which one does each call use?

    Read the code, then choose one answer.

    class Shelf {
    public:
        int& at(int i);         // (A)
        int at(int i) const;    // (B)
    };
    
    const Shelf c;
    Shelf m;
    c.at(0);
    m.at(0);
    Show the answer: Question 1 of 2

    Answer: c.at(0) calls (B) and m.at(0) calls (A)

    A const object can call only the const version, and a plain object takes the version whose this needs no added const. Standard containers use exactly this pair: at() on a const vector gives read-only access.

  2. Question 2 of 2 Follow-up: balance() const should also count how often it was called, in a member reads_. How can a const function change reads_?

    Choose one answer.

    Show the answer: Question 2 of 2

    Answer: Declare reads_ mutable, because the count does not change what the account represents

    A const member function may not change the object's ordinary members. A mutable member is the exception, for data such as a counter that is not part of the object's value: two calls to the const balance() leave reads_ at 2.

Two design questions start from this lesson: an account class whose public setter let a balance go negative, and a const_cast proposed to make a call through a const reference compile:

Model answers open with any pass: ₹79 for 1 week ($2.99), one payment that never renews.

  1. Fresher to mid

    An Account class exposes setBalance(long long) and getBalance(), and a production bug left one balance negative. How would you redesign the class so that this cannot happen again, and how would you move hundreds of existing callers to the new interface safely?

    Model answer, Pro. Model answers open with any pass; prices are in the notice on this page. First step and follow-ups

  2. Mid to senior

    A function takes const Order& and fails to compile because it calls order.total(), which is not const. A teammate proposes const_cast to make it build. What do you do instead, and when is const_cast actually safe?

    Model answer, Pro. First step and follow-ups

Key takeaways

  • A class keeps its data members private and offers public member functions, so every change goes through code that checks it and the account’s rule holds after every call.
  • Access is checked by the compiler, per class rather than per object, and costs nothing at run time: the account is 8 bytes, the size of its one member, and struct differs from class only in its default access.
  • Inside a member function, this points to the object before the dot, and returning *this by reference lets calls be chained on the same object.
  • A const member function promises not to change the object, and const objects and const references can call only such functions, so every function that only reads should be marked const.
  • Operations rather than setters, a check before every change and a valid start keep a class’s invariant.
  • A function that needs no private data stays outside the class, where it cannot break the invariant.

Terms in this lesson

access specifier
One of the labels public:, private: and protected: in a class. Each applies to the members declared after it, up to the next label; members of a class start out private and those of a struct public.
const member function
A member function with const after its parameter list, as in long long balance() const. It may not change the object, and it is the only kind of member function a const object or a const reference can call.
invariant
A rule about an object's data that holds whenever none of its member functions is running, such as "the balance is never negative". A class keeps it by making the data private and checking every change in its member functions.
member function
A function declared inside a class and called on one object of it with a dot, as in asha.deposit(500). It may use the private members of every object of its class.
this
Inside an ordinary member function, a pointer to the object the function was called on: in a.add(4'000), this is &a. A member name such as total_ in the body means this->total_.

References

Related tools

Report a problem with this lesson

Quick answers and tool search

Type to search tools or to get a quick answer, for example 18% of 2500. Use the up and down arrow keys to move through the results, Enter to choose, and Escape to close.