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:
- release what this object owns (
::close, cannot fail); - install the source's descriptor (an
intwrite); - empty the source (an
intwrite).
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;
}
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 move worked. The descriptor is now
a's andbis empty, anda's own previous descriptor was closed on the way in — the first two lines of the output are the proof that nothing leaked. -
Self-move was a no-op. The check ran before anything was touched, so
astill owns4and the file is intact. -
The naive class lost the file. It closed its descriptor and then
stored the number it had just closed. The object is not corrupt — the number
happens to be
-1now — but the file is gone, and a differently-ordered body would have left a danglingfd_behind instead.
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.