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.