Ch14.6: Move Assignment: Releasing First

中文(简体)

1. Release, Then Steal

Move assignment has the same three parts as copy assignment and the opposite order of difficulty:

Nothing here can fail, which is exactly why the whole operator is noexcept:


	my_posix_file &operator=(my_posix_file &&other) noexcept
	{
		if (this == ::std::addressof(other)) [[unlikely]]
		{
			return *this;
		}
		reset(other.fd_);   // close ours, take theirs
		other.fd_ = -1;     // they own nothing now
		return *this;
	}

reset(other.fd_) is doing two things at once — close the old descriptor and store the new one — which is fine here precisely because neither step can fail. There is no critical line to place, because there is nothing above it: the whole function is below it.

2. Self-Move Needs the Check

Move assignment does need the self-check, and the reason is the mirror image of the reason copy assignment does not.

Copy assignment duplicates first, so when this == &other the duplicate exists before anything is closed (Ch14.4). Move assignment closes first and has no duplicate to fall back on. Run it on itself and the sequence is: close the descriptor, then store the descriptor you just closed, then empty the source — which is also the destination. The file is gone.


	// WRONG: no self-check
	naive_file &operator=(naive_file &&other) noexcept
	{
		if (fd_ != -1)
		{
			::close(fd_);        // *this is now empty
		}
		fd_ = other.fd_;          // other IS *this: the closed number comes back
		other.fd_ = -1;
		return *this;
	}
Copy assignment: no check, because you copy before you destroy.
Move assignment: check, because you destroy before you take.
That asymmetry is not a style preference. It follows directly from which operation is allowed to run first, which follows from the critical line.

3. Both Verified


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

using namespace ::fast_io::io;

class my_posix_file
{
	int fd_{-1};

	void reset(::std::int_least32_t newfd = -1) noexcept
	{
		if (fd_ != -1)
		{
			::close(fd_);
		}
		fd_ = newfd;
	}

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 &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();
	}

	[[nodiscard]] bool is_open() const noexcept
	{
		return fd_ != -1;
	}

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

class naive_file
{
	int fd_{-1};

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

	naive_file(naive_file &&other) noexcept : fd_{other.fd_}
	{
		other.fd_ = -1;
	}

	naive_file &operator=(naive_file &&other) noexcept
	{
		// WRONG: no self-check. When other is *this, the file is closed and
		// the descriptor is then copied from itself as -1.
		if (fd_ != -1)
		{
			::close(fd_);
		}
		fd_ = other.fd_;
		other.fd_ = -1;
		return *this;
	}

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

	[[nodiscard]] bool is_open() const noexcept
	{
		return fd_ != -1;
	}

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

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};
	my_posix_file b{"demo.txt", O_RDONLY};
	println("a = ", a.native_handle(), ", b = ", b.native_handle());
	a = ::std::move(b);
	println("after a = move(b): a = ", a.native_handle(), ", b = ", b.native_handle(), ", b.is_open() = ", b.is_open());
	my_posix_file *alias{&a};
	a = ::std::move(*alias); // self-move through an alias
	println("after self-move:   a = ", a.native_handle(), ", a.is_open() = ", a.is_open());

	naive_file n{"demo.txt", O_RDONLY};
	println("n = ", n.native_handle());
	naive_file *nalias{&n};
	n = ::std::move(*nalias); // same, the naive way
	println("after self-move:   n = ", n.native_handle(), ", n.is_open() = ", n.is_open());
}

a = 3, b = 4
after a = move(b): a = 4, b = -1, b.is_open() = 0
after self-move:   a = 4, a.is_open() = 1
n = 3
after self-move:   n = -1, n.is_open() = 0

The self-move in the program goes through a pointer, ::std::move(*alias), on purpose: that is how self-move happens in real code — through a reference, an index, or a loop over a container — and it is exactly the case a compiler warning cannot see. Writing a = ::std::move(a); literally does get a warning from GCC:


warning: moving 'a' of type 'my_posix_file' to itself [-Wself-move]

Treat that warning as a bug in your code rather than in the class. The class still has to survive it, because the compiler cannot prove the indirect cases away.

4. Move Assignment and Copy Assignment Together

copy assignment operator=(T const&) move assignment operator=(T&&)
first step ::dup — can fail ::close — cannot fail
self-assignment check not needed: the duplicate is taken before the close required: the close happens before the take
exception specification throws noexcept
what the source owns afterwards the same as before; it was copied, not taken nothing: fd_ == -1
exception safety strong (nothing changes if the duplicate fails) nothrow (there is nothing that can fail)

If you ever find yourself unsure which of the two you are writing, look at what the function does to the source. Copies leave it alone; moves empty it. Everything else follows from that.