Ch14.11: The Whole Class, and fast_io’s posix_file

English

提示:本页尚未翻译,当前显示英文原文。

1. The Class, Complete

Everything from the last nine subchapters, in one piece. With #include <fast_io.h> and #include <fast_io_dsal/string_view.h> at the top it is a complete header, and there is no other code in it: one int, six special members, swap, and the two operations that make it a file.


class my_posix_file
{
	int fd_{-1};

public:
	void reset(::std::int_least32_t newfd = -1) noexcept
	{
		if (fd_ != -1)
		{
			::close(fd_);
		}
		fd_ = newfd;
	}

	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);
		}
		// ---- critical line ----
		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_;
	}

	::std::size_t read_some(char *buf, ::std::size_t n) throws
	{
		auto ret{::read(fd_, buf, n)};
		if (ret == -1) [[unlikely]]
		{
			throw throws static_cast<::std::errc>(errno);
		}
		return static_cast<::std::size_t>(ret);
	}

	::std::size_t write_some(char const *buf, ::std::size_t n) throws
	{
		auto ret{::write(fd_, buf, n)};
		if (ret == -1) [[unlikely]]
		{
			throw throws static_cast<::std::errc>(errno);
		}
		return static_cast<::std::size_t>(ret);
	}
};

inline void swap(my_posix_file &a, my_posix_file &b) noexcept
{
	a.swap(b);
}

Read it top to bottom and you can see the chapter: reset is the only ::close (Ch14.2); the copy constructor duplicates (Ch14.3); copy assignment has the critical line (Ch14.4); the move constructor empties its source (Ch14.5); move assignment has the self-check (Ch14.6); swap is three writes (Ch14.7). Everything that only moves a number is noexcept; everything that calls the kernel is throws — the classification of Ch14.9, visible in the declarations.

2. Using It


#include <fast_io.h>
#include <fast_io_dsal/string_view.h>

using namespace ::fast_io::io;

#include <fast_io_dsal/vector.h>

class my_posix_file
{
	int fd_{-1};

public:
	void reset(::std::int_least32_t newfd = -1) noexcept
	{
		if (fd_ != -1)
		{
			::close(fd_);
		}
		fd_ = newfd;
	}

	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_;
	}

	::std::size_t read_some(char *buf, ::std::size_t n) throws
	{
		auto ret{::read(fd_, buf, n)};
		if (ret == -1) [[unlikely]]
		{
			throw throws static_cast<::std::errc>(errno);
		}
		return static_cast<::std::size_t>(ret);
	}

	::std::size_t write_some(char const *buf, ::std::size_t n) throws
	{
		auto ret{::write(fd_, buf, n)};
		if (ret == -1) [[unlikely]]
		{
			throw throws static_cast<::std::errc>(errno);
		}
		return static_cast<::std::size_t>(ret);
	}
};

inline void swap(my_posix_file &a, my_posix_file &b) noexcept
{
	a.swap(b);
}

int main()
{
	{
		my_posix_file out{"demo.txt", O_WRONLY | O_CREAT | O_TRUNC};
		out.write_some("herbceptions\n", 13);
	}

	my_posix_file in{"demo.txt", O_RDONLY};
	char buf[32]{};
	auto n = in.read_some(buf, sizeof(buf));
	print("read back: ", ::fast_io::mnp::os_c_str_with_known_size(buf, n));

	my_posix_file copy{in};
	println("in = ", in.native_handle(), ", copy = ", copy.native_handle());

	::fast_io::vector<my_posix_file> files;
	files.push_back(::std::move(copy));
	println("the vector holds ", files.size(), " file, fd = ", files.front().native_handle());

	swap(in, files.front());
	println("after swap: in = ", in.native_handle(), ", files.front() = ", files.front().native_handle());
}

read back: herbceptions
in = 3, copy = 4
the vector holds 1 file, fd = 4
after swap: in = 4, files.front() = 3

The class works with everything else in this tutorial: it goes into a ::fast_io::vector because its move is noexcept, it survives the container's reallocation because move assignment releases what it must, and swap moves ownership between the vector and a local without a single system call.

3. Where Each Rule Was Taught

In the listingRuleSubchapter
int fd_{-1};the empty state is part of the designCh14.1
~my_posix_file() noexceptone place releases, and it cannot failCh14.2
my_posix_file(my_posix_file const &)a copy is a second descriptorCh14.3
operator=(my_posix_file const &)the critical lineCh14.4
my_posix_file(my_posix_file &&)take, then empty the sourceCh14.5
operator=(my_posix_file &&)release, then take; self-checkCh14.6
void swap(my_posix_file &) noexceptthe primitive a commit is made ofCh14.7
the if (fd == -1) checksthe empty state is valid, not an errorCh14.10

4. What fast_io’s posix_file Adds

The real thing lives in include/fast_io_hosted/platforms/posix/file.h, and it is basic_posix_family_file, which is the same class with the parts a library needs and a tutorial does not:

None of those additions change a single rule on these pages. That is the point of learning them on a fifty-line class: the fifth rule of a five-thousand-line class is the same rule.

5. The Rule of Zero

Before you write any of this, check whether you have to. If every member of your class is already a well-behaved value — an int, a ::fast_io::string, a ::fast_io::vector — then the compiler's copies, moves and destructor are all correct, and you should write none of them:


class config
{
	::fast_io::string name_;
	::fast_io::vector<::fast_io::string> keys_;
	// nothing else: the five special members are generated, and they are right
};

This is why Chapter 13 never mentioned any of it: a class of numbers and strings needs none. The rule of five is the price of owning a raw resource, and the cheapest way to pay it is to not own one — put the int inside a class that already implements the five rules, and let composition do the work.

6. Checklist

7. Mistakes, and What They Look Like

MistakeSymptom
No destructor descriptors accumulate until ::open fails with EMFILE
Member-wise copy of a handle double ::close, or closing a descriptor that now belongs to another file
Move constructor that does not empty the source two owners of one descriptor; the same double-close, later and harder to find
Release before acquire in copy assignment a failed assignment destroys the object it was assigning to
No self-check in move assignment self-move silently closes the file
Self-check in copy assignment (if (this == &other)) harmless, but it hides the fact that the ordering was never fixed
Move constructor not noexcept static assertion failed: vector element types must be nothrow move constructible or trivially relocatable
Destructor that reports destructor cannot be declared with a herbceptions ('throws'/'return_failure{E}') exception specification
Destructor that prints call to a 'throws' or 'return_failure{E}' function must be handled by an enclosing 'try { } catch throws'
Copy of a moved-from file [posix](9)Bad file number at run time — by design, not by accident

8. What Comes Next

This class is written for one character type and one platform family. The next chapter turns it into a template so the same code serves char, char8_t, wchar_t and the Windows backends — and you will recognise every line of it, because templates do not change the five rules, they only repeat them for more types.

The other direction is Chapter 16: <<, == and friends can be written for a class like this one, and they raise exactly the questions about copying, moving and failing that you have just answered.