Ch14.10: Self-Assignment and Moved-From Objects

English

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

1. Self-Assignment, Both Kinds

a = a happens in real code more often than people expect: through a reference, an index (v[i] = v[j] with i == j), a template that does not know the two arguments are the same object. The two assignments treat it differently, and both are correct:

self-copy a = aself-move a = std::move(a)
needs a check no yes
why the duplicate is taken before anything is closed the old resource is released before anything is taken
observable effect the descriptor number may change; the file does not nothing at all

The full argument is in Ch14.4 and Ch14.6; this subchapter is about the state the object is in after a move, which is the other half of the same subject.

2. The Contract of a Moved-From Object

The standard says a moved-from object is valid but unspecified: you may destroy it, assign to it, and call members whose preconditions it meets — nothing more. “Unspecified” is not a licence to be mysterious, though: a class should document what its moved-from state is, and ours is:

QuestionAnswer for my_posix_file
is_open()false
native_handle()-1
destructordoes nothing, closes nothing
assignment (copy or move)works, and makes the object open again
read_some / write_somethrows ::std::errc::bad_file_descriptor
copy construction from itthrows ::std::errc::bad_file_descriptor

The last two rows are the reason the copy operations take the values they take: they are not being defensive, they are applying ::dup to -1 and forwarding what the kernel said.

3. All of It, Running


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

using namespace ::fast_io::io;

// A move constructor that forgets to empty the source.
class sloppy_file
{
	int fd_{-1};

public:
	explicit sloppy_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;
	}

	sloppy_file(sloppy_file &&other) noexcept : fd_{other.fd_} // WRONG: other.fd_ is left alone
	{}

	~sloppy_file() noexcept
	{
		if (fd_ != -1)
		{
			::close(fd_);
		}
	}

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

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()
{
	{ // make sure the example file exists
		::std::int_least32_t fd{::open("demo.txt", O_WRONLY | O_CREAT, 0644)};
		::close(fd);
	}

	my_posix_file a{"demo.txt", O_RDONLY};
	println("a = ", a.native_handle());
	a = a; // self copy: no check needed
	println("after a = a: a = ", a.native_handle(), ", is_open = ", a.is_open());

	my_posix_file b{"demo.txt", O_RDONLY};
	my_posix_file *alias{&b};
	b = ::std::move(*alias); // self move through an alias: the check is needed
	println("after self-move: b = ", b.native_handle(), ", is_open = ", b.is_open());

	my_posix_file source{"demo.txt", O_RDONLY};
	my_posix_file moved{::std::move(source)};
	println("moved = ", moved.native_handle(), ", source = ", source.native_handle(), ", source.is_open() = ", source.is_open());

	char buf[8]{};
	try
	{
		source.read_some(buf, 4); // fd -1
	}
	catch throws (::std::error e)
	{
		print("reading a moved-from file: ", e, "\n");
	}
	try
	{
		my_posix_file copy{source};
	}
	catch throws (::std::error e)
	{
		print("copying a moved-from file: ", e, "\n");
	}

	source = my_posix_file{"demo.txt", O_RDONLY}; // assignment revives it
	println("after reviving: source = ", source.native_handle(), ", is_open = ", source.is_open());

	{
		sloppy_file x{"demo.txt", O_RDONLY};
		println("sloppy x = ", x.native_handle());
		{
			sloppy_file y{::std::move(x)};
			println("sloppy y = ", y.native_handle(), ", x still says ", x.native_handle());
		}
		println("after y died, the descriptor is closed and x still thinks it owns it");
	}
}

a = 3
after a = a: a = 4, is_open = 1
after self-move: b = 3, is_open = 1
moved = 5, source = -1, source.is_open() = 0
reading a moved-from file: [posix](9)Bad file number
copying a moved-from file: [posix](9)Bad file number
after reviving: source = 6, is_open = 1
sloppy x = 7
sloppy y = 7, x still says 7
after y died, the descriptor is closed and x still thinks it owns it

The first three lines repeat the self-assignment results. The interesting part starts at moved = 5, source = -1: the source is empty but alive, and every use of it is a clean, catchable failure rather than undefined behaviour.

Then look at the last three lines, which come from a deliberately broken sloppy_file whose move constructor forgets other.fd_ = -1:


	sloppy_file(sloppy_file &&other) noexcept : fd_{other.fd_} // WRONG
	{}

Two objects now believe they own descriptor 7. The inner one dies first and closes it, so the outer one is holding a number that is no longer open — and the next ::open anywhere in the program will be handed that same number and be closed by the wrong owner. The move constructor must empty its source.

4. ::std::move Is a Cast

Nothing is moved by ::std::move. It is an unconditional cast to T&&, whose only effect is that overload resolution prefers the move constructor or move assignment:


template <typename T>
constexpr ::std::remove_reference_t<T> &&move(T &&t) noexcept
{
	return static_cast<::std::remove_reference_t<T> &&>(t);
}

Every consequence in this chapter follows from that definition:

A moved-from object is not “dead”, and a moved-from my_posix_file is not an error state. It is the empty state of the class, the same one a default-constructed file is in. Do not use it for anything else without saying so in the docs — and where you do document it, as with the table above, a moved-from object is as ordinary as any other.