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.

The compiler's copy is member-wise, and a member-wise copy of a handle is always wrong. This is the reason the rule of five exists at all: you cannot take the generated copy and the generated destructor. Once one of the five is yours, you own all of them.

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.

Why not close-then-open the path again? Because the file need not still be at that path, and because reopening would lose the file offset and the open mode. ::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.

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.

Copying is a promise about ownership, not about bytes. “Can this be copied?” and “what does the copy mean?” are the same question. If you cannot answer the second in one sentence, delete the copy.