Ch14.11: The Whole Class, and fast_io’s posix_file
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 listing | Rule | Subchapter |
|---|---|---|
int fd_{-1}; | the empty state is part of the design | Ch14.1 |
~my_posix_file() noexcept | one place releases, and it cannot fail | Ch14.2 |
my_posix_file(my_posix_file const &) | a copy is a second descriptor | Ch14.3 |
operator=(my_posix_file const &) | the critical line | Ch14.4 |
my_posix_file(my_posix_file &&) | take, then empty the source | Ch14.5 |
operator=(my_posix_file &&) | release, then take; self-check | Ch14.6 |
void swap(my_posix_file &) noexcept | the primitive a commit is made of | Ch14.7 |
the if (fd == -1) checks | the empty state is valid, not an error | Ch14.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:
-
It is a template.
familyandch_typemean the same code servesposix_file,u8posix_file,wposix_fileand the Win32/NT families. That is a job for Chapter 15. -
It is an
io_observerbase plus a file. The observer knows how to read, write, scatter, gather and query the descriptor; the file adds ownership on top. fast_io's whole design is this split between “handle” and “owner of a handle”. -
The path is a template parameter.
::fast_io::constructible_to_os_c_straccepts acstring_view, a::fast_io::string, or a plain literal, and converts to whatever the platform wants. We take a::fast_io::cstring_viewand nothing else, so the example stays readable. -
It has a
close()that reports.::closecan fail, and a caller may care; so the class offersvoid close() throwsnext to thenoexceptdestructor — the split from Ch14.2. - It knows about Windows. The same file carries the Cygwin, Wine, NT and Win32 constructors, which is most of its length. Our reduced version has none of it.
-
It deletes the dangerous conversions. Constructing a file from an
io_observerwould make two owners out of one handle, so that constructor is= delete, exactly as Ch14.3 recommends.
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
- Does the class own a resource? If not, write none of the five.
- Is there a sentinel for “owns nothing”, initialised in the member?
- Is there exactly one place that releases it, and is that place
noexcept? - Does the destructor handle the empty state without calling a system function?
- Does the copy constructor produce a new resource, or is copy deleted?
- Does copy assignment do all fallible work before the critical line?
- Does move assignment empty its source?
- Does move assignment check for self-assignment?
- Is
swapnoexcept, and is there a freeswapas well? - Can you name the exception-safety level of every member function?
7. Mistakes, and What They Look Like
| Mistake | Symptom |
|---|---|
| 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.