I'm creating an implementation of std::optional
in C++14. However, I'm slightly confused with how the move constructor is specified. Here's what I'm referring to (emphasis mine):
The expression inside
noexcept
is equivalent tois_nothrow_move_constructible_v<T>
. This constructor shall not participate in overload resolution unlessis_move_constructible_v<T>
istrue
. Ifis_trivially_move_constructible_v<T>
istrue
, this constructor shall be a constexpr constructor.
What does it mean to remove the move constructor from overload resolution? Deletion and SFINAE don't seem to work for this scenario.
std::move is actually just a request to move and if the type of the object has not a move constructor/assign-operator defined or generated the move operation will fall back to a copy.
std::move is used to indicate that an object t may be "moved from", i.e. allowing the efficient transfer of resources from t to another object. In particular, std::move produces an xvalue expression that identifies its argument t . It is exactly equivalent to a static_cast to an rvalue reference type.
The compiler will automatically generate move constructors for your simple classes, similarly as it defines copy constructor.
std::move itself does "nothing" - it has zero side effects. It just signals to the compiler that the programmer doesn't care what happens to that object any more. i.e. it gives permission to other parts of the software to move from the object, but it doesn't require that it be moved.
Use inheritance. Define a base class template and specialize it for different T
constructability characteristics, marking the move-constructor delete
as needed.
template<bool>
struct enable_move {};
template<>
struct enable_move<true> {
constexpr enable_move() noexcept = default;
constexpr enable_move(enable_move&&) noexcept = default;
};
template<>
struct enable_move<false> {
constexpr enable_move() noexcept = default;
constexpr enable_move(enable_move&&) noexcept = delete;
};
template<typename T>
struct optional : private enable_move<is_move_constructible<T>::value> {
// . . .
};
The complexity comes when combining this with other special members with similar requirements, like the copy/move constructors and copy/move assignment operators.
For this reason e.g. in libstdc++, optional
derives from _Enable_copy_move
(see 1, 2), which takes care of all 4 special members at the same time.
Yes, SFINAE does not work for constructors, use base classes forcing the compiler to do the right thing.
It means it is not defined and the class cannot be move constructed. More interesting question is why is it needed?
I am not 100% sure I have the right answer to that.
TL;DR Returning std::optional<NonMoveable>
generates compiler errors if the move constructor of optional
is present. On the other hand, returning NonMoveable
directly fallbacks to copy constructor.
First, the constraint does not break anything. The constructor cannot be implemented if T
cannot be move constructed.
Second, all methods of std::optional
are very tricky due to std::optional<std::optional<T>>
issue which could easily lead to ambiguous calls if proper constraints are not taken, optional(U&& value)
is really susceptible to this.
The main reason is, I believe, because we want optional<T>
to act as T
whenever possible and there is one edge case, that I am aware of, when the existence of std::optional
's move constructor for non-moveable T
leads to unnecessary compiler errors. Coincidentally, it is the case of returning std::optional
by value from functions, something I do very often.
For a variable x
of type T
, return x
in a function T foo()
calls move constructor if it accessible, copy if not.
Take these simple definitions:
#include <utility>
struct CopyOnly {
CopyOnly() = default;
CopyOnly(const CopyOnly &) = default;
CopyOnly(CopyOnly &&) = delete;
CopyOnly &operator=(const CopyOnly &) = default;
CopyOnly &operator=(CopyOnly &&) = delete;
~CopyOnly() = default;
};
template <typename T> struct Opt {
Opt() = default;
Opt(const Opt &other) : m_value(other.m_value) {}
Opt(Opt &&other) : m_value(std::move(other.m_value)) {
// Ordinary move ctor.
// Same as =default, just writing for clarity.
}
// Ignore how `T` is actually stored to be "optional".
T m_value;
};
and this example
template <typename T> T foo(const T &t) {
auto x = t;
return x;
}
int main() {
Opt<int> opt_int;
CopyOnly copy;
Opt<CopyOnly> opt_copy;
foo(opt_int);//#1
foo(copy);//#2
foo(opt_copy);//#3
}
return x
:
opt_int
can be moved.Opt<CopyOnly>
has accessible move constructor so it is chosen, but its instantiation leads to an error due m_value(std::move(other.m_value))
trying to explicitly calls deleted move ctor.If one disables the move constructor, copy constructor is chosen and the code is identical to #2.
It means it doesn't exist in c++14. In c++20 you can do this with requires clauses.
You can get fancy with default and inherited bases etc; if you inherit from a class without a move ctor, you don't have one. Things that don't exist don't participate in overload resolution.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With