Ch13.11: Putting It Together

中文(简体)

1. The Finished Class

This is the same account that grew through the chapter. It now uses every idea in it: a private invariant, a private constructor behind a named one, checked modifiers, const accessors, chaining through *this, class-wide static data and functions, and one friend.


#include <fast_io.h>
#include <cstdint>
#include <cstddef>
using namespace ::fast_io::io;

class account
{
public:
	// ---- creation: a named constructor, the only door into the class ----
	static account open(::std::int_least64_t opening) throws
	{
		if (opening < 0)
		{
			throw throws ::std::errc::invalid_argument;
		}
		return account{opening};
	}

	// ---- the interface: what anybody may do with an account ----
	::std::int_least64_t balance() const noexcept
	{
		return balance_;
	}
	::std::size_t id() const noexcept
	{
		return id_;
	}

	account &deposit(::std::int_least64_t amount) throws
	{
		if (amount <= 0)
		{
			throw throws ::std::errc::invalid_argument;
		}
		balance_ += amount;
		return *this;
	}
	account &withdraw(::std::int_least64_t amount) throws
	{
		if (amount <= 0 || amount > balance_)
		{
			throw throws ::std::errc::invalid_argument;
		}
		balance_ -= amount;
		return *this;
	}

	// ---- facts about the class itself ----
	static ::std::size_t opened() noexcept
	{
		return next_id_ - 1;
	}
	static constexpr ::std::size_t iban_length{34};

	// ---- an outside function we let in ----
	friend bool richer(account const &a, account const &b) noexcept
	{
		return a.balance_ > b.balance_;
	}

private:
	// Only open() may call this; nobody else can even name it.
	explicit account(::std::int_least64_t opening) noexcept
		: balance_{opening}, id_{next_id_++}
	{
	}

	::std::int_least64_t balance_{};
	::std::size_t id_{};
	inline static ::std::size_t next_id_{1};
};

int main()
{
	try
	{
		auto a{account::open(100)};
		auto b{account::open(250)};

		a.deposit(50).deposit(25);
		b.withdraw(100);

		println("a: id = ", a.id(), ", balance = ", a.balance());
		println("b: id = ", b.id(), ", balance = ", b.balance());
		println("richer(a, b) = ", richer(a, b));
		println("accounts opened = ", account::opened(), ", iban length = ", account::iban_length);

		a.withdraw(1000); // breaks the invariant, so it is refused
	}
	catch throws (::std::error e)
	{
		print("refused: ", e, "\n");
	}
	println("the program keeps running");
}

a: id = 1, balance = 175
b: id = 2, balance = 150
richer(a, b) = 1
accounts opened = 2, iban length = 34
refused: [posix](22)Invalid argument
the program keeps running

2. Reading It Back

In the class What it is doing From
static account open(...) throws The only way in: a named constructor that validates before the object exists, and the only caller of the private constructor. Ch13.9
explicit account(...) noexcept after private: Builds the object from a value that open has already checked. Hidden so that nobody can skip the check. Ch13.4, Ch13.6
balance() const noexcept A read-only accessor returning a value, callable on a const object. Ch13.3
deposit(...) throws, withdraw(...) throws The only two ways the balance may change, each checking the invariant before writing, each reporting refusal with ::std::errc::invalid_argument. Ch13.5
return *this; with return type account & Call chaining: a.deposit(50).deposit(25); Ch13.7
inline static ::std::size_t next_id_{1}; One identifier source shared by every account, touched only by the constructor. Ch13.8
static ::std::size_t opened() noexcept A fact about the class, callable without any object. Ch13.9
friend bool richer(account const &, account const &) A symmetric comparison that the class chose to allow, so that the balances stay private. Ch13.10

Note what is not in the class: no public data member, no setter for the balance, no getter that hands out a reference, no way to make an account without going through a check. Every line outside the class in main could be written by somebody who has never seen the definition, and they still cannot produce an invalid account.

3. The Design Checklist

When you write a class of your own, walk this list once:

  1. Write the invariant as one sentence. If you cannot, the type is probably two types, or it is a plain struct and needs no wall.
  2. Make the data private and everything else private until proven needed. The interface is the small set of names you will keep.
  3. Make every constructor establish the invariant, and make a validating constructor throws. Validate first, then write.
  4. Give every read-only member function const, and return values (or const &), never a writable reference to the storage.
  5. Give the class a name for each way of building it when the constructors would otherwise be a puzzle: open, from_history, parse.
  6. Mark every member function noexcept or throws. If a member function can fail, its caller must deal with it; that is information the signature has to carry.
  7. Use static members for facts about the class, and treat any mutable static member as shared state that needs a reason.
  8. Open the wall with friend only for operations that are symmetric between objects, after checking that a member function or an accessor would not do.

4. Mistakes That Cost the Most

Mistake What it costs Do instead
Private data, plus a getter and a setter that accepts anything. The wall is decorative: the invariant is still unenforced, and the class is longer. Name the operations (deposit, withdraw) and check inside them.
Forgetting const on the accessors. Nobody can pass your type to a function that takes T const &, so every caller becomes non-const and the safety evaporates. Add const the moment you write a member function that does not write.
Returning ::std::int_least64_t & from an accessor. a.balance() = -1000; compiles, and the invariant is gone. Return by value, or const & for large members.
Assigning in the constructor body instead of the initializer list. Members are written twice, the list order stops matching, and members that cannot be assigned stop working. Initialize in the member initializer list, in declaration order.
Single-argument constructors without explicit. Silent conversions turn a wrong call into a wrong object. explicit, unless the conversion is the point of the type.
A public constructor that does not check, next to a checking factory. Half the objects in the program are valid and half are not, and the invariant is worthless. One door. Make the unchecked constructor private.
Catching a herbception and continuing as if the operation happened. The caller believes the deposit succeeded. Silent wrong answers are worse than a stop. Handle it, propagate it, or let it reach the top and stop the program.

5. Where You Are Now

You can define a type that owns its data, decides what the outside may do, keeps a promise, and reports failure in the one way this tutorial uses everywhere: throws and catch throws (::std::error e). That is enough to build the rest of the language on. Everything that follows — operators, generic code, the standard library’s own types — is written in terms of classes like this one.

The one thing to carry forward. A class is a promise. private is what makes the promise believable, const is what makes it readable, a constructor is what makes it true from the first moment an object exists, and throws is what happens when somebody asks for something the promise cannot allow.