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.
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.