Home > database >  With std::optional, what does it mean to "remove the move constructor from overload resolution&
With std::optional, what does it mean to "remove the move constructor from overload resolution&

Time:11-27

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 to is_nothrow_move_constructible_v<T>. This constructor shall not participate in overload resolution unless is_move_constructible_v<T> is true. If is_trivially_move_constructible_v<T> is true, 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.

CodePudding user response:

You define a base class template and specialize it for different T constructibility characteristics.

For example see libstdc .

CodePudding user response:

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.

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 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 store 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:

  1. Calls move constructor because opt_int can be moved.
  2. Calls copy constructor as a fallback because it cannot be moved.
  3. Compiler error because 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.

Yes, SFINAE does not work for constructors, use base classes forcing the compiler to do the right thing.

CodePudding user response:

It means it doesn't exist in . In 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.

  • Related