Ch14.10: Self-Assignment and Moved-From Objects
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 = a | self-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:
| Question | Answer for my_posix_file |
|---|---|
is_open() | false |
native_handle() | -1 |
| destructor | does nothing, closes nothing |
| assignment (copy or move) | works, and makes the object open again |
read_some / write_some | throws ::std::errc::bad_file_descriptor |
| copy construction from it | throws ::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:
- without a move constructor, the cast changes nothing and the copy runs (Ch14.5);
- on a
constobject the cast producesT const &&, which no move constructor accepts, so the copy runs again; ::std::move(x)never modifiesx; the constructor or assignment that follows does.
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.