Ch14.3: The Copy Constructor: Two Objects, Two Resources
1. What a Copy Has to Mean
The destructor answers “who releases it”. The copy constructor answers the next question: when a new object is made from an existing one, what does the new object own?
For point in Chapter 13 the answer was
trivial: its own two numbers. For an owner the answer must be
“its own resource”, and the only way to get a second
descriptor for the same open file is to ask the kernel for one:
::dup.
int dup(int fd); // a new descriptor for the same open file
int dup2(int fd, int newfd); // make newfd refer to the same open file
2. What the Compiler Writes If You Say Nothing
A class with no copy constructor gets one generated for it, and the generated one does
the same thing it does for point: it copies each member. For an
int that means the number is copied. The number is not the descriptor
— it is only the name of one.
#include <fast_io.h>
#include <fast_io_dsal/string_view.h>
using namespace ::fast_io::io;
// No copy constructor, no move constructor, no assignment: the compiler
// supplies member-wise versions of all of them.
class shallow_file
{
int fd_{-1};
public:
explicit shallow_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;
}
~shallow_file() noexcept
{
if (fd_ != -1)
{
::close(fd_);
}
}
[[nodiscard]] ::std::int_least32_t native_handle() const noexcept
{
return fd_;
}
};
int main()
{
shallow_file a{"demo.txt", O_RDONLY};
println("a = ", a.native_handle());
{
shallow_file b{a}; // copies the number, not the file
println("b = ", b.native_handle(), ", b owns the same descriptor = ", b.native_handle() == a.native_handle());
} // b closes the descriptor it thinks it owns
println("after b died, a = ", a.native_handle(), ", still valid = ", ::fcntl(a.native_handle(), F_GETFD) != -1);
shallow_file c{"demo.txt", O_RDONLY};
println("c = ", c.native_handle(), ", the number came back");
} // c and a both close descriptor 3
a = 3
b = 3, b owns the same descriptor = 1
after b died, a = 3, still valid = 0
c = 3, the number came back
b has the same int, so it has the same owner's claim, so it
closes the descriptor when it dies. After that line a still holds
3 and 3 is no longer open: a is a
dangling owner. Every later use of it fails with
EBADF, and its destructor will close descriptor 3 a second
time — which by then may belong to a completely different file, as the last two
lines of the output show. Closing somebody else's file is one of the nastiest bugs
there is, because the symptom appears far away from the mistake.
3. The Copy Constructor: A Descriptor of Its Own
my_posix_file(my_posix_file const &other) throws : fd_{::dup(other.fd_)}
{
if (fd_ == -1) [[unlikely]]
{
throw throws static_cast<::std::errc>(errno);
}
}
Read it in three parts.
-
The parameter is
const&. A copy constructor must not modify what it copies; if it did,my_posix_file const ¶meters could not be copied, andconstobjects would be second-class. -
::duphappens in the member initializer list. It is the only place the member is initialized (Ch13.6), and doing the fallible call there means nothing has been modified yet when it runs. -
throws, because::dupcan fail. It fails withEMFILEwhen the process has too many descriptors open and withEBADFwhen the source has none — which is exactly what copying a moved-from file does (Ch14.10).
::dup duplicates the open file,
not the name, so it works after the file has been renamed or unlinked.
4. Two Descriptors, One Open File
What exactly does the copy share? The program below writes ten bytes to a file, then copies a reader, then reads four bytes from each handle in turn:
#include <fast_io.h>
#include <fast_io_dsal/string_view.h>
using namespace ::fast_io::io;
class my_posix_file
{
int fd_{-1};
public:
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() noexcept
{
if (fd_ != -1)
{
::close(fd_);
}
}
[[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);
}
};
int main()
{
{
my_posix_file out{"demo.txt", O_WRONLY | O_CREAT | O_TRUNC};
out.write_some("0123456789", 10);
}
my_posix_file a{"demo.txt", O_RDONLY};
my_posix_file b{a}; // copy: a descriptor of its own, same open file description
println("a = ", a.native_handle(), ", b = ", b.native_handle());
char buf[16]{};
a.read_some(buf, 4);
println("read from a: ", ::fast_io::mnp::os_c_str_with_known_size(buf, 4));
b.read_some(buf, 4);
println("read from b: ", ::fast_io::mnp::os_c_str_with_known_size(buf, 4));
my_posix_file c{"demo.txt", O_RDONLY};
c.read_some(buf, 4);
println("read from c: ", ::fast_io::mnp::os_c_str_with_known_size(buf, 4));
}
a = 3, b = 4
read from a: 0123
read from b: 4567
read from c: 0123
Look at the three reads.
-
aandbhave different descriptors (3and4), so closing one does not close the other, and each one's destructor closes exactly one descriptor. That is what makes the copy safe. -
But they share the file offset: after
aread0123,b's read continues at4567, not at the beginning.::dupduplicates the open file description, and the offset is part of it. -
cwas opened separately, so it has its own open file description and reads0123from the start.
So a copy of my_posix_file is an independent owner of a shared
stream. That is the honest semantics of the underlying system, and the class
should not pretend otherwise. If you need an independent offset, open the file again
(c), do not copy.
5. When Copying Makes No Sense
For a file, duplicating the descriptor is meaningful, so we support it. For plenty of
resources it is not: a lock is either held or not, a buffer has one owner, an
::open in progress cannot be duplicated. Such a class says so:
class my_lock
{
public:
my_lock(my_lock const &) = delete;
my_lock &operator=(my_lock const &) = delete;
};
Anyone who tries to copy it gets a compile-time refusal instead of a double
::close at run time:
error: use of deleted function 'my_lock::my_lock(const my_lock&)'
note: declared here
fast_io does the same where ownership cannot be duplicated: its
posix_file_factory — the object that holds a fresh descriptor until
a file object takes it over — deletes its copy constructor and copy assignment
for exactly this reason.