Ch14.7: swap: Exchanging Two Owners
1. What swap Has to Do
For a resource owner, swap exchanges which resource each object owns. It
does not copy, does not close, does not allocate: it exchanges two handles.
void swap(my_posix_file &other) noexcept
{
::std::int_least32_t const tmp{fd_};
fd_ = other.fd_;
other.fd_ = tmp;
}
Three int writes and a temporary. It cannot fail, so it is
noexcept — and that is not a detail: swap being
noexcept is what makes copy and
swap give the strong guarantee.
2. The Free Function, and Why It Exists
A member function is enough for a.swap(b). The free function is what makes
plain swap(a, b) work:
inline void swap(my_posix_file &a, my_posix_file &b) noexcept
{
a.swap(b);
}
It lives in the same namespace as the class, which is where argument-dependent lookup
(Ch13.10) will find it. Generic code that says
swap(x, y) — not ::std::swap(x, y) — then picks
yours when the type has one, and falls back to
::std::swap otherwise. Write both: the member does the work, the free
function makes the name findable.
3. What ::std::swap Does Without Yours
The standard ::std::swap is implemented with a move and two move
assignments:
template <typename T>
void swap(T &a, T &b) noexcept(...)
{
T tmp{::std::move(a)};
a = ::std::move(b);
b = ::std::move(tmp);
}
For our class that is three descriptor exchanges, so it produces the same result
— the program below uses all three spellings and prints the same numbers for
each. It is only correct because the move operations are noexcept and
because move assignment releases what it should; if any of those were wrong,
::std::swap would be wrong too, in a place far from the bug.
#include <fast_io.h>
#include <fast_io_dsal/string_view.h>
using namespace ::fast_io::io;
class my_posix_file
{
int fd_{-1};
void reset(::std::int_least32_t newfd = -1) noexcept
{
if (fd_ != -1)
{
::close(fd_);
}
fd_ = newfd;
}
public:
my_posix_file() noexcept = default;
explicit my_posix_file(::fast_io::cstring_view path, ::std::int_least32_t flags) throws
{
::std::int_least32_t fd{::open(path.c_str(), flags, 0644)};
if (fd == -1) [[unlikely]]
{
throw throws static_cast<::std::errc>(errno);
}
fd_ = fd;
}
my_posix_file(my_posix_file const &other) throws : fd_{::dup(other.fd_)}
{
if (fd_ == -1) [[unlikely]]
{
throw throws static_cast<::std::errc>(errno);
}
}
my_posix_file &operator=(my_posix_file const &other) throws
{
::std::int_least32_t newfd{::dup(other.fd_)};
if (newfd == -1) [[unlikely]]
{
throw throws static_cast<::std::errc>(errno);
}
reset(newfd);
return *this;
}
my_posix_file(my_posix_file &&other) noexcept : fd_{other.fd_}
{
other.fd_ = -1;
}
my_posix_file &operator=(my_posix_file &&other) noexcept
{
if (this == ::std::addressof(other)) [[unlikely]]
{
return *this;
}
reset(other.fd_);
other.fd_ = -1;
return *this;
}
~my_posix_file() noexcept
{
reset();
}
void swap(my_posix_file &other) noexcept
{
::std::int_least32_t const tmp{fd_};
fd_ = other.fd_;
other.fd_ = tmp;
}
[[nodiscard]] bool is_open() const noexcept
{
return fd_ != -1;
}
[[nodiscard]] ::std::int_least32_t native_handle() const noexcept
{
return fd_;
}
};
inline void swap(my_posix_file &a, my_posix_file &b) noexcept
{
a.swap(b);
}
static_assert(::std::is_nothrow_swappable_v<my_posix_file>);
int main()
{
{ // make sure the example file exists
::std::int_least32_t fd{::open("demo.txt", O_WRONLY | O_CREAT, 0644)};
::close(fd);
}
my_posix_file a{"demo.txt", O_RDONLY};
my_posix_file b{"demo.txt", O_WRONLY};
println("a = ", a.native_handle(), ", b = ", b.native_handle());
a.swap(b);
println("after a.swap(b): a = ", a.native_handle(), ", b = ", b.native_handle());
swap(a, b);
println("after swap(a, b): a = ", a.native_handle(), ", b = ", b.native_handle());
::std::swap(a, b);
println("after ::std::swap(a, b): a = ", a.native_handle(), ", b = ", b.native_handle());
my_posix_file empty;
a.swap(empty);
println("after a.swap(empty): a = ", a.native_handle(), ", empty = ", empty.native_handle());
println("a.is_open() = ", a.is_open(), ", empty.is_open() = ", empty.is_open());
}
a = 3, b = 4
after a.swap(b): a = 4, b = 3
after swap(a, b): a = 3, b = 4
after ::std::swap(a, b): a = 4, b = 3
after a.swap(empty): a = -1, empty = 4
a.is_open() = 0, empty.is_open() = 1
Read the last two lines: swapping with a default-constructed (empty) object is how you
release a resource and keep it alive somewhere else. a
ends up owning nothing and empty owns the file; when
empty dies, the file closes. That trick is worth remembering — it is
how you park a resource in a local object and let a destructor take care of it.
4. Checking That It Really Cannot Fail
::std::is_nothrow_swappable_v is a compile-time question with a
compile-time answer, and it is the property containers and algorithms test for:
static_assert(::std::is_nothrow_swappable_v<my_posix_file>);
If you had written swap by copying instead — for instance
my_posix_file tmp{other}; other = *this; *this = tmp; — the
assertion would fail, because copying a file can fail, and rightly so.
5. swap Is the Commit Primitive
Copy assignment (Ch14.4) ended with
a single infallible statement after the critical line. For a class with one resource
that statement was reset(newfd). For a class with several members there is
no single reset, but there is always a swap — and that
is the subject of the next subchapter.