Ch14: The Rule of Five: Copy, Move, Swap
1. Why This Chapter Exists
Every class in Chapter 13 owned numbers. Copying one was
obviously harmless: point b{a} gives you two points, and nothing is
shared. account owned a balance, and copying it was still harmless.
The moment a class owns something the operating system handed out — a file descriptor, a socket, a lock, a block of memory — that comfort ends. Two objects that own the same descriptor are a bug waiting to happen, and the compiler's default behaviour makes exactly that bug: it copies the number, not the resource.
Such a class has to answer five questions, and this chapter is those five answers.
2. What a Resource Is
A resource is anything that
- has to be acquired before use and released afterwards,
- can be acquired only a limited number of times,
- is identified by a handle that you pass around instead of the thing itself.
A file descriptor is the smallest useful example. ::open returns an
int; ::close gives it back; the process has a limited number
of them; and the int is a handle, not the file. Copying the handle does
not copy the file — it gives you a second name for the same thing, and
two owners of one resource is precisely the problem this chapter solves.
3. The Five Special Member Functions
When you do not write them, the compiler writes them for you. When you write one, the rest change their meaning. They are:
| Member | Called when | Subchapter |
|---|---|---|
destructor ~T() |
an object goes out of scope, or is deleted | Ch14.2 |
copy constructor T(T const&) |
a new object is made from an existing one | Ch14.3 |
copy assignment operator=(T const&) |
an existing object is given another object's value | Ch14.4 |
move constructor T(T&&) |
a new object is made from one that is about to die | Ch14.5 |
move assignment operator=(T&&) |
an existing object takes over from one that is about to die | Ch14.6 |
swap |
you ask for it by name | Ch14.7 |
Five of them are special members the compiler will generate; swap is not
— nobody calls it unless you write the call. It belongs here anyway, because it
is the one operation that lets copy assignment be safe
(Ch14.8), and because
::std::swap falls back to three moves, which is only fast and safe when
your moves are noexcept.
4. The Example: my_posix_file
Every page from here on uses one running example: a class that owns a POSIX file
descriptor. It is a reduced version of what fast_io itself does in
basic_posix_file, keeping only what the five rules need, so that the
interesting code is never more than fifteen lines.
class my_posix_file
{
int fd_{-1};
public:
my_posix_file() noexcept = default;
explicit my_posix_file(::fast_io::cstring_view path, ::std::int_least32_t flags) throws;
my_posix_file(my_posix_file const &other) throws;
my_posix_file &operator=(my_posix_file const &other) throws;
my_posix_file(my_posix_file &&other) noexcept;
my_posix_file &operator=(my_posix_file &&other) noexcept;
~my_posix_file() noexcept;
void swap(my_posix_file &other) noexcept;
[[nodiscard]] bool is_open() const noexcept;
[[nodiscard]] ::std::int_least32_t native_handle() const noexcept;
::std::size_t read_some(char *buf, ::std::size_t n) throws;
::std::size_t write_some(char const *buf, ::std::size_t n) throws;
};
The path is a ::fast_io::cstring_view, the flags are the plain POSIX
int flags (O_RDONLY, O_WRONLY | O_CREAT, ...),
and the body calls ::open, ::dup and ::close
directly. Nothing is hidden behind fast_io helpers, so every rule this chapter states
can be checked against the code on the page.
Failures are reported the only way this tutorial reports failures (Ch3.16):
if (fd == -1) [[unlikely]]
{
throw throws static_cast<::std::errc>(errno);
}
On a POSIX system the standard ::std::errc values are the
errno values, so the cast preserves the error exactly; the handler sees
[posix](2)No such file or directory and friends.
5. The Two Ideas That Pay for the Chapter
The rules themselves are bookkeeping. Two ideas make them worth learning, and they are the reason this chapter exists at all.
The critical line
Inside a function that may fail, there is a line — sometimes an obvious one, sometimes a single statement — such that
- above it: things may fail, and nothing has been modified yet;
- below it: nothing may fail, and the modifications happen.
Put every fallible step above the line and every change below it, and the operation either happens completely or not at all. That is the strong exception safety guarantee, and it is what Ch14.4 is really about.
Four levels of exception safety
nothrow, strong, basic, none
— every function you write has exactly one of these, whether you thought about
it or not. Ch14.9 names them,
classifies every member of my_posix_file, and shows how herbceptions turn
the first level from a promise into something the compiler proves.
6. Conventions Used in This Chapter
- Data members that are private end with an underscore:
fd_. -
Every member function is either
noexceptorthrows— there is no third option in this tutorial (Ch6.16). Which one it is tells you the exception-safety level before you read the body. -
Integers are
::std::int_least32_t,::std::int_least64_tor::std::size_t; never plainint. The only exception isint main(), and the POSIX API's ownintarguments and return values, which are shown as theintthey are. -
Failures are herbceptions:
throw throws ...andcatch throws (::std::error e). -
Every complete program on these pages was compiled with
-std=c++26 -fherbceptions -lherbceptionsand run; the output shown is what it actually printed.
7. Contents
- Ch14.1: Ownership: Why a Class Needs a Destructor
- Ch14.2: The Destructor
- Ch14.3: The Copy Constructor
- Ch14.4: Copy Assignment and the Critical Line
- Ch14.5: The Move Constructor
- Ch14.6: Move Assignment
- Ch14.7: swap
- Ch14.8: Copy and Swap
- Ch14.9: Exception Safety: nothrow, strong, basic, none
- Ch14.10: Self-Assignment and Moved-From Objects
- Ch14.11: The Whole Class, and fast_io's posix_file