Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

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.

like image 254
itzjackyscode Avatar asked Nov 26 '21 23:11

itzjackyscode


People also ask

Does STD move move constructor?

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.

What happens when you to std :: move?

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.

Are move constructors automatically generated?

The compiler will automatically generate move constructors for your simple classes, similarly as it defines copy constructor.

Is std :: move required?

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.


3 Answers

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.

like image 157
rustyx Avatar answered Oct 17 '22 06:10

rustyx


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.

like image 34
Quimby Avatar answered Oct 17 '22 04:10

Quimby


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.

like image 2
Yakk - Adam Nevraumont Avatar answered Oct 17 '22 04:10

Yakk - Adam Nevraumont