Register

C++ for Beginners

0 of 24 notes read 0%

Reading progress is saved to your INFIRALS account. Sign in to keep this note marked as read.

Const correctness and encapsulation

Classes and object lifetime12 min readLesson 4 of 4

4.1 Keep the rules with the data

Encapsulation keeps an object's data consistent by routing every change through member functions. The class knows which combinations of values are valid, and the public interface offers operations that preserve them. A caller cannot set a balance directly and skip the withdrawal rule.

The public part of a class is a contract. It should be small enough to understand and stable enough that callers can rely on it. Data members usually stay private. Accessors expose values for reading, and named operations handle changes that need checks.

cpp
#include <iostream>
class Account {
public:
    explicit Account(int balance) : balance_(balance > 0 ? balance : 0) {}
    bool withdraw(int amount) {
        if (amount <= 0 || amount > balance_) return false;
        balance_ -= amount;
        return true;
    }
    int balance() const { return balance_; }
private:
    int balance_;
};

int main() {
    Account account(100);
    std::cout << std::boolalpha << "First: " << account.withdraw(30) << "\n";
    std::cout << "Second: " << account.withdraw(200) << "\nBalance: " << account.balance() << "\n";
    return 0;
}

Output

First: true
Second: false
Balance: 70

4.1.1 Access levels and friends

public members are part of the interface. private members are reachable only from the class's own member functions and friends. protected members are also reachable from derived classes, which the next module covers. A class starts private; a struct starts public.

A friend declaration gives a named function or class access to private members. It is useful for a comparison function that needs two objects' internals, or for an operator that cannot be a member. The class grants friendship; the friend cannot take it. Keep it rare, because it widens the interface beyond the member functions.

cpp
#include <iostream>

class Account {
public:
    explicit Account(int balance) : balance_(balance) {}
    friend bool richer(const Account& a, const Account& b);
private:
    int balance_;
};

bool richer(const Account& a, const Account& b) {
    return a.balance_ > b.balance_;
}

int main() {
    Account first(80);
    Account second(120);
    std::cout << std::boolalpha << "Second richer: " << richer(second, first) << "\n";
    return 0;
}

Output

Second richer: true

4.1.2 Const member functions

A const member function promises not to change the object's visible state. The compiler enforces the promise by treating the object as const inside the function, so an assignment to a data member fails. Const objects and const references can call only the functions that carry the marker.

Mark getters, display functions, and comparisons const when they do not change the object. The marker also lets the same object be passed to functions that take a const reference. A const-correct class is easier to use in larger programs because callers do not have to guess which functions are safe to call on shared data.

cpp
#include <iostream>

class Row {
public:
    explicit Row(int value) : value_(value) {}
    int& first() { return value_; }
    const int& first() const { return value_; }
private:
    int value_;
};

int main() {
    Row row(5);
    row.first() = 9;
    const Row locked(3);
    std::cout << row.first() << " " << locked.first() << "\n";
    return 0;
}

Output

9 3

4.1.3 Mutable and logical constness

Sometimes a const member function needs to update bookkeeping that callers cannot observe. A cache, a hit counter, or a lazily computed value can change while the object's logical state stays the same. The mutable keyword allows exactly that: a mutable member can change inside a const member function.

Use mutable for data that supports the function's result and is not part of the value the object represents. A member that holds the object's main state should stay non-mutable. If a const function changes something a caller can see, the function is not really const, and the design should change instead.

cpp
#include <iostream>

class Sensor {
public:
    explicit Sensor(int value) : value_(value) {}
    int read() const { ++reads_; return value_; }
    int reads() const { return reads_; }
private:
    int value_;
    mutable int reads_ = 0;
};

int main() {
    Sensor sensor(21);
    std::cout << "Reading: " << sensor.read() << "\n";
    std::cout << "Reads: " << sensor.reads() << "\n";
    return 0;
}

Output

Reading: 21
Reads: 1

4.1.4 Do not hand out the internals

A getter that returns a non-const reference gives callers direct access to a private member. They can change the vector, the string, or the number without going through the class, and the class cannot check the result. The private keyword then protects the name while the value can change without checks.

Return a copy when the value is small, or a const reference when it is large and read-only. If callers must edit the collection, offer named operations such as add_member, remove_member, or replace_member. The class keeps control of its rules and still provides the access callers need.

cpp
#include <iostream>
#include <string>
#include <vector>

class Team {
public:
    void add(const std::string& name) { members_.push_back(name); }
    const std::vector<std::string>& members() const { return members_; }
private:
    std::vector<std::string> members_;
};

int main() {
    Team team;
    team.add("Ada");
    team.add("Ravi");
    const std::vector<std::string>& names = team.members();
    std::cout << "Members: " << names.size() << "\n";
    return 0;
}

Output

Members: 2

4.1.5 Common mistakes

  • Making data members public because a getter and a setter feel like extra work.
  • Forgetting const on a getter, then discovering that a const object cannot use it.
  • Returning a non-const reference from a getter.
  • Marking a function const and then changing visible state through mutable.
  • Adding a friend to avoid designing a proper public operation.
  • Giving every member its own getter and setter, which exposes the representation without protecting anything.

4.1.6 Practice

  1. Write a Counter class with a private count and public increment, reset, and value functions.
  2. Make value const and call it on a const object.
  3. Add a friend function that compares two counters by their private counts.
  4. Add a mutable read counter to a const getter and print it after two reads.
  5. Replace a public vector member with a private one and a const reference getter.

Interview questions

3 questions from this note. Open one to see the answer; no account needed.

  • 01What does a const member function promise?

    That it will not modify the object's data members, apart from mutable ones. The compiler enforces it, and the function can be called on const objects and through const references.

  • 02Why return a const reference to a private vector instead of a non-const reference?

    A non-const reference lets callers change the vector without going through the class, so the class cannot keep its rules. A const reference allows reading without copying and keeps the data under the class's control.

  • 03When is `mutable` appropriate?

    When a const member function must update bookkeeping that is invisible to callers, such as a cache or an access counter. Data that forms part of the object's visible state should stay non-mutable.