Contents

Chapter 19

Constructors, and Types That Can't Be Broken

Initialization, invariants, and types that enforce their own rules.

Chapter 18’s Deck had a flaw and the stretch pointed at it:

Deck d;
std::cout << d.size();   // 0

A brand-new deck has no cards in it. It only becomes a real deck once someone calls fill(), and nothing makes them. The class carefully prevents outsiders adding a 53rd card while permitting a deck of zero.

Every rule a type has is worth nothing if the type can be born breaking it.

A function that runs on creation

class Deck {
public:
    Deck() {
        std::vector<std::string> suits{
            "Clubs", "Diamonds", "Hearts", "Spades"};
        for (const std::string& s : suits) {
            for (int r = 2; r <= 14; ++r) {
                cards.push_back(Card(r, s));
            }
        }
    }

    int size() const { return cards.size(); }

private:
    std::vector<Card> cards;
};
Deck d;
std::cout << "fresh deck: " << d.size() << '\n';
fresh deck: 52

Deck() is a constructor. Two things mark it out: it has the same name as the class, and it has no return type: not void, nothing at all. It isn’t a function you call. It’s the code that runs when a Deck comes into existence.

fill() is gone, and so is the possibility of forgetting it. There is no longer any point in the deck’s life at which it holds the wrong number of cards.

That is the whole idea of this chapter, and it’s worth stating plainly: move the work from the user of the type to the type itself. Anything a caller must remember to do is something they will eventually forget.

Constructors that take arguments

Most types need to be told something when they’re made:

class Player {
public:
    Player(std::string n, int s) : name(n), score(s) {}

    std::string describe() const {
        return name + ": " + std::to_string(score);
    }

private:
    std::string name;
    int score;
};
Player p("Ada", 3);
std::cout << p.describe() << '\n';
Ada: 3

The odd part is : name(n), score(s) between the brackets and the body. That’s a member initializer list, and it means “start name off as n, and score as s”.

You could write it in the body instead, as name = n;, and for these types it would work identically. Prefer the list anyway, for two reasons that will matter later: it initialises the fields rather than creating them empty and then assigning over the top, and some types can only be set up this way. It also puts all the setting-up in one glanceable place above the body.

Read the syntax as punctuation and don’t overthink it. A colon, then each field with its starting value in brackets, comma-separated.

Enforcing a rule at the door

Here’s where constructors stop being convenience and start being correctness:

class Temperature {
public:
    Temperature(double c) : celsius(c) {
        if (celsius < -273.15) {
            celsius = -273.15;
        }
    }

    double value() const { return celsius; }

private:
    double celsius;
};
Temperature t(-400);
std::cout << t.value() << '\n';
-273.15

Nothing colder than absolute zero can exist, so no Temperature object holding less than -273.15 can exist either. The field is private, so the constructor is the only way in, and the constructor checks.

The thing that must stay true, celsius is never below -273.15, is called an invariant. Naming it is most of the work; once you can state it in a sentence, where to enforce it is usually obvious.

Clamping is one response. Refusing is another, and chapter 17’s tools apply: a function that returns std::optional<Temperature> can decline to make one at all. Which is right depends on whether a nonsensical input is a small mistake or a serious one. What isn’t right is storing it and hoping.

More than one way to build something

A class can have several constructors, as long as each takes different arguments:

class Fraction {
public:
    Fraction() : num(0), den(1) {}
    Fraction(int n) : num(n), den(1) {}
    Fraction(int n, int d) : num(n), den(d) {}

    std::string text() const;

private:
    int num;
    int den;
};
Fraction();        // 0/1
Fraction(3);       // 3/1
Fraction(3, 4);    // 3/4
0/1 3/1 3/4

C++ picks by counting and matching the arguments, the same way it decided which function you meant back in chapter 9’s “requires 2 arguments, but 1 was provided” error. Writing the no-argument one yourself is how you get Fraction f; back after the callout above took it away.

Don’t get carried away. Each constructor is another way the type can be built and another thing to keep correct; three is plenty and one is often right.

When the answer is “you can’t have one”

Clamping works for temperatures because -400 has an obvious nearest legal value. Some invariants have no such fallback. A fraction with a denominator of zero isn’t a slightly-wrong fraction. It isn’t a fraction.

A constructor is badly placed to refuse. It has no return value to say no with, and the object is already being created by the time it runs.

So don’t use one. Put a function in front of it:

std::optional<Fraction> make_fraction(int n, int d) {
    if (d == 0) {
        return std::nullopt;
    }
    return Fraction(n, d);
}
make_fraction(1, 2);   // 1/2
make_fraction(1, 0);   // refused
1/2
refused

Chapter 17’s std::optional again, in a new place: the caller cannot get at a Fraction without first dealing with the possibility that there isn’t one. The constructor stays simple, the check happens once, and no zero-denominator Fraction is ever built.

This pairing, a plain constructor plus a function that decides whether to call it, is worth remembering. Use the constructor alone when every input can be made sensible; put a function in front when some inputs are simply refusals.

What not to do: const members

You may wonder whether a field that never changes should be marked const:

private:
    const std::string suit;

It compiles and it does guarantee the value never changes. It also quietly makes the whole type non-assignable, and since std::sort works by moving elements around, a std::vector of them can no longer be sorted. On my machine that mistake produced 375 lines of errors from inside the standard library, with barely a mention of the class that caused it.

A private field with no setter is already unchangeable from outside, which is the protection you actually wanted. Don’t use const members, and if you meet one in someone else’s code and their vector won’t sort, you now know why.

Exercise 1 · Three constructors

Rewrite chapter 18’s Thermometer so the constructor takes the starting temperature and clamps it, and there is no setter at all. Confirm that Thermometer t; no longer compiles, and read the error.

Write a class Rectangle taking width and height, with area() const and perimeter() const. Make the constructor refuse negative sides by clamping them to zero.

Then write a class Fraction taking a numerator and denominator. A denominator of zero is the invariant to defend, and deciding what to do about it is the interesting part. Clamping to 1 is a lie, so consider what else you could do.

Check yourself

1. What marks a constructor out from an ordinary member function?

Yes. Not even void. It is not called by name. It runs when an object of that type is created.

Not quite. It returns nothing. The object exists as a result of it running, rather than being handed back.

Not quite. It can go anywhere in the public section. Putting it first is a common habit, not a rule.

2. You add a constructor taking two arguments. Elsewhere, Thing t; stops compiling. Why?

Not quite. It can have as many as you like, provided they take different arguments. Adding a second no-argument one fixes this.

Yes. Once you have said how the type is built, C++ stops guessing. Deliberate, and confusing the first time because the error appears in untouched code.

Not quite. They are public like anything else you want callable from outside.

3. What is an invariant?

Not quite. That is one way to get one, but an invariant can involve fields that change constantly, such as a balance that must never go negative.

Yes. State it in a sentence and the place to enforce it usually becomes obvious: the constructor, plus any function that could break it.

Not quite. The compiler enforces access, not meaning. Keeping the invariant true is your code’s job, and privacy just limits how much code can get it wrong.

Project

War

Roughly 60 minutes

The card game. Two players, one card each per round, higher card wins. It is a game of no skill whatsoever, which makes it perfect for this. All the interesting decisions are in the types.

Round 1: Q of Hearts vs 9 of Hearts -- you win
Round 2: 3 of Clubs vs Q of Clubs -- they win
Round 3: A of Clubs vs K of Hearts -- you win
Round 4: Q of Diamonds vs 2 of Spades -- you win
Round 5: 2 of Diamonds vs 8 of Spades -- they win

Final: you 3, them 2
42 cards left

Change Card first, and this is the design decision the whole project turns on. Chapter 18 stored the rank as a string, which is fine for printing and useless for comparing: "10" < "9" is true, because strings compare alphabetically.

So store the rank as an int from 2 to 14, and convert for display:

class Card {
public:
    Card(int r, std::string s) : rank(r), suit(s) {}

    int value() const { return rank; }
    std::string name() const { return rank_name() + " of " + suit; }

private:
    int rank;
    std::string suit;

    std::string rank_name() const;
};

Note rank_name() is private. It’s a helper for name() and nobody outside needs it. Member functions can be private too, and it’s worth doing, because it keeps the public section down to what the type is for.

rank_name is an if chain: 11 is J, 12 is Q, 13 is K, 14 is A, and anything else is std::to_string(rank).

Deck gets a constructor that builds all 52, replacing chapter 18’s fill(). The rank loop becomes for (int r = 2; r <= 14; ++r), which is neater than the vector of strings it replaces.

The game itself is a loop you could have written in chapter 8: deal two, compare with value(), count the winner, and remember the tie case.

Stretch one, play the whole deck. Twenty-six rounds instead of five. The loop condition becomes deck.size() >= 2, which is the honest test.

Stretch two, real War. A tie means each player deals three face-down and one face-up, and the winner takes everything on the table. You’ll need a std::vector<Card> for the pile and to think about what happens when someone runs out mid-war. This is a genuine step up in difficulty and the first problem in this book where the rules are the hard part rather than the C++.