Search code examples
c++c++14language-lawyeroption-type

With std::optional, what does it mean to "remove the move constructor from overload resolution"?


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.


Solution

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

    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.