Contents

Chapter 18

Classes: Types You Design Yourself

Member functions, and why data goes private.

Chapter 14 gave you a Contact and admitted its weakness in a callout: nothing stops anyone writing ada.age = -4. The struct holds three values and has no opinion about them.

For a contact book that’s fine. It stops being fine the moment your type has rules: a deck that must contain 52 cards, a date that can’t be the 40th of March, a bank balance nobody should be able to set directly.

This chapter is about types that hold their own rules.

Putting the function inside

Here is a Card, and a function that describes it:

struct Card {
    std::string rank;
    std::string suit;
};

std::string name(const Card& c) {
    return c.rank + " of " + c.suit;
}
std::cout << name(ace) << '\n';

Now the same thing with the function moved inside the braces:

class Card {
public:
    std::string rank;
    std::string suit;

    std::string name() const {
        return rank + " of " + suit;
    }
};
std::cout << ace.name() << '\n';
Ace of Spades

That’s a member function, and it closes a loop that has been open since chapter 9. Back then you were told some functions are called thing.action() and others action(thing), and that the dotted ones belong to the value. Now you can make them.

Two things changed and both matter.

The parameter is gone. Inside a member function, the fields are just there. rank means “the rank of whichever card this was called on”. You don’t pass the card in, because the call already said which one.

There’s a const after the brackets. That is a promise that this function doesn’t change the card, and it is the same promise as chapter 12’s const&, written in a new place. Leave it off and the function still works, but then you can’t call it on a const Card&, which is what every function in chapter 14 was taking. Get in the habit: a member function that only reads gets const.

Shutting the door

So far this is rearrangement. Here’s the part that changes what’s possible:

class Counter {
public:
    void bump() { ++count; }
    int value() const { return count; }

private:
    int count = 0;
};

public: and private: are labels, and everything after one obeys it until the next. Public members are reachable from outside; private ones are reachable only from inside the class’s own functions.

Try to reach in from outside and the compiler stops you:

n.count = 100;
error: 'count' is a private member of 'Counter'
note: declared private here

Short, clear, and pointing at both ends of the problem, a pleasant change from chapter 12’s wall of library noise.

Note int count = 0; while you’re here. A field can be given a starting value right where it’s declared, and every Counter then begins at zero without anyone remembering to do it. That is chapter 14’s uninitialised-garbage trap, closed for good.

class and struct

class Card { ... };
struct Card { ... };

The difference is one thing: struct members are public unless you say otherwise, class members are private unless you say otherwise. That’s it. There is no deeper distinction, no separate feature set, and anything you can do with one you can do with the other.

Convention is worth following anyway. Use struct for a plain bag of fields with no rules, such as chapter 14’s Contact, and class when the type is meant to look after itself. A reader seeing class expects a private: section somewhere, and you may as well meet the expectation.

From here on this book uses class when there’s something to protect.

What to make private

The instinct after learning this is to make every field private and add a get_ and set_ for each. Resist it. A private field with a setter that changes it to anything is a public field wearing a hat, and you’ve written six lines to achieve nothing.

The useful question is: could an outsider put this object into a state that makes no sense?

Privacy is worth its cost when there’s an invariant, a thing that must stay true. No invariant, no need.

Bodies outside the class

So far every member function has had its body written inside the braces. That’s fine while they’re one-liners and gets unreadable fast. A class with six ten-line functions in it is a wall you have to scroll past to see what the type even offers.

The alternative is to declare inside and define outside:

class Card {
public:
    std::string name() const;
    void flip();
    bool face_up() const { return up; }

    std::string rank;
    std::string suit;

private:
    bool up = false;
};

std::string Card::name() const {
    if (!up) { return "(face down)"; }
    return rank + " of " + suit;
}

void Card::flip() {
    up = !up;
}
(face down)
Ace of Spades

Card::name is the new punctuation. The :: says this name is the one belonging to Card. Without it you’d be defining a free function that happens to share the name and knows nothing about ranks or suits.

Three details worth having:

This is exactly what chapter 20 needs. When a class moves into a header, the declarations stay there and the bodies go to a .cpp, and this is the split that makes it possible.

Reading a class from the outside in

One practical habit. When you meet a class in somebody’s code, read the public: section first and stop there.

That section is what the type can do. The private part is how, and you can usually avoid caring. This is the same reason chapter 9’s functions were worth writing: you can use name() without holding its four lines in your head, and now you can do that for a whole type.

It’s also a decent test of your own design. If your public section reads like a sensible list of things one might want, the class is probably right. If it reads like a list of your fields with get_ in front of them, it probably isn’t.

Exercise 1 · Make one look after itself

Write a class Thermometer with a private double celsius, a set(double), a celsius_value() const and a fahrenheit() const that converts.

Make set refuse anything below -273.15, printing a complaint and leaving the old value alone. Then try to break it from main by assigning to the field directly, and read the error.

Now notice something: fahrenheit() is computed, not stored. There is no double fahrenheit field and there shouldn’t be, because two fields that must agree with each other are two chances to disagree.

Check yourself

1. What is the difference between class and struct in C++?

Not quite. Both hold both. Chapter 14s struct could have had member functions all along.

Yes. That is the only difference in the language. The rest is convention about which to reach for.

Not quite. A struct can have a private: label and behave identically. The keywords differ only in where they start.

2. Why write const after a member function’s brackets?

Not quite. Invented. It is a promise about modification, not about how often you call it.

Yes. Without it, none of chapter 12s const& parameters could call the function, which is most of them by now.

Not quite. A different position entirely. const before the return type would be about the result; after the brackets it is about the object.

3. When is making a field private actually worth it?

Not quite. A Card with a public rank and suit is honest and fine. A rule you cannot state is a rule you do not have.

Yes. The invariant is the reason. No invariant means a private field plus a setter that accepts anything, which achieves nothing.

Not quite. The type of the field has nothing to do with it. What matters is whether an outsider could put the object into a nonsensical state.

Project

A deck of cards

Roughly 60 minutes

A deck that cannot be wrong. Chapter 19 deals it into a game; this chapter builds it.

deck has 52 cards
  Q of Diamonds
  J of Hearts
  9 of Spades
  7 of Spades
  A of Clubs
deck has 47 cards

Card is a struct. Rank and suit, public, plus a name() const that joins them. No rules, no privacy.

Deck is a class, and its std::vector<Card> cards is private. That is the whole design: outsiders can shuffle and deal, and cannot add a 53rd card.

class Deck {
public:
    void fill();
    void shuffle();
    Card deal();
    int size() const;

private:
    std::vector<Card> cards;
};

Write the public section first and look at it before writing any bodies. Four things a deck can do, and nothing about how.

Filling it is two vectors of strings, thirteen ranks and four suits, and a loop inside a loop. The outer over suits, the inner over ranks, pushing a Card each time. 4 × 13 is where the 52 comes from, and you should check that it does.

Shuffling is std::shuffle, which is std::sort’s cousin and takes the random generator from chapter 9:

std::random_device seed;
std::mt19937 generator(seed());
std::shuffle(cards.begin(), cards.end(), generator);

Dealing takes from the end, because taking from the end of a vector is cheap and taking from the front is not:

Card top = cards.back();
cards.pop_back();
return top;

back() gives you the last element and pop_back() removes it: two functions chapter 10 didn’t need and this does.

Stretch one, deal from an empty deck. As written, deal() on an empty deck reads off the end, which is chapter 10’s undefined behaviour with a nicer name. Decide what should happen and make it happen. std::optional<Card> from chapter 17 is one honest answer; refusing to compile is not available to you here.

Stretch two, the deck that fills itself. Right now a Deck starts empty and is only correct once somebody remembers fill(). That is exactly the kind of rule a type should enforce rather than hope for, and chapter 19 is about the mechanism that fixes it. Leave the flaw in place and notice it.