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:
-
Write the invariant as one sentence. If you cannot, the type
is probably two types, or it is a plain
structand needs no wall. - Make the data private and everything else private until proven needed. The interface is the small set of names you will keep.
-
Make every constructor establish the invariant, and make a
validating constructor
throws. Validate first, then write. -
Give every read-only member function
const, and return values (orconst &), never a writable reference to the storage. -
Give the class a name for each way of building it when the
constructors would otherwise be a puzzle:
open,from_history,parse. -
Mark every member function
noexceptorthrows. If a member function can fail, its caller must deal with it; that is information the signature has to carry. - Use static members for facts about the class, and treat any mutable static member as shared state that needs a reason.
-
Open the wall with
friendonly 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.
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.