Ch14.1: Ownership: Why a Class Needs a Destructor

English

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

1. A Descriptor Is a Resource

::open hands you an int. It is not a number you may do arithmetic on; it is a handle on something the kernel owns. Three properties make it a resource:

2. What a Bare int Does

A plain variable has no idea it owns anything. Nothing closes it, so it lives until the process dies:


#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() noexcept
	{
		if (fd_ != -1)
		{
			::close(fd_);
		}
	}

	[[nodiscard]] ::std::int_least32_t native_handle() const noexcept
	{
		return fd_;
	}
};

int main()
{
	// A bare int does not know it owns an operating system resource.
	::std::int_least32_t raw{::open("demo.txt", O_WRONLY | O_CREAT | O_TRUNC, 0644)};
	println("bare int: ", raw);

	// Nothing closes everything. The descriptor is leaked for the rest of the run.
	my_posix_file managed{"demo.txt", O_RDONLY};
	println("managed: ", managed.native_handle());

	{
		my_posix_file scoped{"demo.txt", O_RDONLY};
		println("in scope: ", scoped.native_handle());
	} // the destructor runs here
	my_posix_file after{"demo.txt", O_RDONLY};
	println("after: ", after.native_handle());
}

Output:


bare int: 3
managed: 4
in scope: 5
after: 5

Read the numbers. The bare int took descriptor 3 and stayed there for the whole run, so every later open got a fresh number: 4, then 5. The managed file inside the block took 5 and, when the block ended, gave 5 back — which is why the next open receives 5 again and not 6.

That reuse is the proof that the destructor ran. Nothing in the program calls ::close explicitly; the object is responsible for its own descriptor, and the responsibility is discharged automatically.

Owning means: everybody who can reach the object, however they leave the scope, gets the resource released. A return, a throw throws, a break, a normal fall-through — the destructor runs in all of them. An explicit ::close at the end of a function runs in none of the first three.

3. The Five Questions

A class that owns a descriptor has to say what happens in these five situations. There is no escape: if you say nothing, the compiler answers all five for you, and its answers are wrong for a resource.

QuestionWritten asWrong default
When the object dies, who frees the descriptor? ~my_posix_file() nobody: the descriptor leaks
When a new object is made from an existing one, what happens? my_posix_file(my_posix_file const &) the number is copied: two owners, one descriptor
When an existing object is assigned from another, what happens? operator=(my_posix_file const &) the old descriptor leaks and the new one is shared
When a dying object is used to build a new one, can we just take it? my_posix_file(my_posix_file &&) it copies instead, which is correct but wasteful
When an existing object takes over from a dying one, what happens? operator=(my_posix_file &&) it copies instead, and leaks the old descriptor

Write all five and you have the rule of five. Write none and have the compiler generate all of them — the rule of zero — which is the better outcome, but only possible when every member is already a well-behaved value. A raw descriptor is not (Ch14.11 comes back to this).

4. What We Are Going to Build

The class stays small on purpose. It holds one int, and everything else is the five rules:


class my_posix_file
{
	int fd_{-1}; // -1 means "no descriptor"; it is the empty state

public:
	// 1. create: acquire a descriptor from a path
	explicit my_posix_file(::fast_io::cstring_view path, ::std::int_least32_t flags) throws;

	// 2. destroy: give the descriptor back                   -> Ch14.2
	~my_posix_file() noexcept;

	// 3. copy: a second descriptor for the same open file     -> Ch14.3
	my_posix_file(my_posix_file const &other) throws;

	// 4. copy-assign: replace what this object owns           -> Ch14.4
	my_posix_file &operator=(my_posix_file const &other) throws;

	// 5. move: take the descriptor and leave -1 behind        -> Ch14.5, Ch14.6
	my_posix_file(my_posix_file &&other) noexcept;
	my_posix_file &operator=(my_posix_file &&other) noexcept;

	// 6. swap: exchange two objects' descriptors              -> Ch14.7
	void swap(my_posix_file &other) noexcept;
};

The sentinel -1 is doing real work: it is the difference between “I own a file” and “I own nothing”. Without it there would be no way to leave a moved-from object in a state where its destructor can run safely.