Ch14.5: The Move Constructor: Taking the Resource
1. What Moving Means for an Owner
Copying has to produce a second descriptor, because both objects survive. Moving does not: the source is about to be destroyed, so the destination can simply take the descriptor and leave the source empty.
No system call, no second entry in the kernel's table, no failure: two
int writes. That is the entire reason the language added
&& parameters.
my_posix_file(my_posix_file &&other) noexcept : fd_{other.fd_}
{
other.fd_ = -1; // the source no longer owns anything
}
Both lines matter. Without the second one there would be two owners of the descriptor: the destination would close it, and then the source's destructor would close the same number again — possibly after it had been handed to an unrelated file.
2. If You Do Not Write It, std::move Copies
This is the trap that catches everybody once. ::std::move moves nothing;
it is a cast to an rvalue reference, and all it does is make the move constructor
eligible. If there is no move constructor, the copy constructor is used
instead, and it is not an error:
#include <fast_io.h>
#include <fast_io_dsal/string_view.h>
using namespace ::fast_io::io;
class only_copy
{
int fd_{-1};
public:
explicit only_copy(::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;
}
only_copy(only_copy const &other) throws : fd_{::dup(other.fd_)}
{
if (fd_ == -1) [[unlikely]]
{
throw throws static_cast<::std::errc>(errno);
}
}
~only_copy() 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);
}
only_copy a{"demo.txt", O_RDONLY};
println("a = ", a.native_handle());
only_copy b{::std::move(a)}; // no move constructor: this calls the copy constructor
println("b = ", b.native_handle(), ", a = ", a.native_handle(), ", a.is_open() = ", a.is_open());
}
a = 3
b = 4, a = 3, a.is_open() = 1
a still owns 3 and b owns a fresh duplicate
4. The move silently became a copy — a wasted system call for a file,
and a wasted deep copy for anything bigger.
3. Why noexcept Is Not Optional
A move constructor is noexcept in our class, and in every serious class
that owns a resource. It is not politeness. Containers reallocate by moving their
elements, and they can only do that if a move cannot fail halfway through the
reallocation. fast_io's containers check this at compile time:
error: static assertion failed: vector element types must be nothrow move constructible
or trivially relocatable
Change the move constructor's noexcept to throws and that is
the diagnostic you get for
::fast_io::vector<my_posix_file>. The container is not being
pedantic; it has no way to put the elements back if the fifth move of a reallocation
fails.
With herbceptions the promise is even stronger than usual. A noexcept
function cannot call a throws function unless it handles the failure
itself:
error: call to a 'throws' or 'return_failure{E}' function must be handled by an enclosing
'try { } catch throws', or the calling function needs a herbceptions specification
so the error can propagate
So noexcept is not a promise you can break at run time; it is a claim the
compiler verifies by looking at every call in the body. A move constructor could not
call ::dup even if you wanted it to.
4. The Source Is Empty, Not Dead
After the move, other.fd_ is -1, and that is a
valid state of the class, not a broken one:
- its destructor runs and closes nothing;
is_open()returnsfalse;- assigning a new file to it makes it open again;
- reading, writing or copying it throws
::std::errc::bad_file_descriptor(Ch14.10).
That state is why the class has a default constructor and the -1 member
initializer in the first place: a moved-from object must be able to live out its
lifetime without doing anything.
5. In Use
The program moves a file into a new object, then puts two of them in a
::fast_io::vector, which needs the noexcept move to accept
the type at all:
#include <fast_io.h>
#include <fast_io_dsal/string_view.h>
#include <fast_io_dsal/vector.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(my_posix_file const &other) throws : fd_{::dup(other.fd_)}
{
if (fd_ == -1) [[unlikely]]
{
throw throws static_cast<::std::errc>(errno);
}
}
my_posix_file(my_posix_file &&other) noexcept : fd_{other.fd_}
{
other.fd_ = -1;
}
~my_posix_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_;
}
};
static_assert(::std::is_nothrow_move_constructible_v<my_posix_file>);
static_assert(::std::is_copy_constructible_v<my_posix_file>);
static_assert(!::std::is_nothrow_copy_constructible_v<my_posix_file>);
static_assert(::std::is_nothrow_destructible_v<my_posix_file>);
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());
my_posix_file b{::std::move(a)};
println("b = ", b.native_handle(), ", a = ", a.native_handle(), ", a.is_open() = ", a.is_open());
::fast_io::vector<my_posix_file> files;
files.push_back(my_posix_file{"demo.txt", O_RDONLY});
files.push_back(my_posix_file{"demo.txt", O_RDONLY});
println("the vector holds ", files.size(), " files:");
for (my_posix_file const &f : files)
{
println(" fd = ", f.native_handle());
}
}
a = 3
b = 3, a = -1, a.is_open() = 0
the vector holds 2 files:
fd = 4
fd = 5
The static_asserts at the top are the chapter in five lines: the type is
nothrow-move-constructible, copy-constructible, not nothrow-copy-constructible
(copying a file genuinely can fail), and nothrow-destructible. Read them as the class's
contract; the rest of the program is a demonstration that the contract holds.
6. Moving From a const Object Copies
A move constructor takes T&&, which cannot bind to a
const object: taking the descriptor out would modify it. So
my_posix_file const & arguments are copied, and
::std::move on a const object quietly does nothing:
my_posix_file const c{"demo.txt", O_RDONLY};
my_posix_file d{::std::move(c)}; // calls the copy constructor
// c is still open
It is correct, and it is also why taking parameters by
my_posix_file (by value) and then ::std::move-ing them into
members works: the copy happens at the call, the move happens in the constructor.