5

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.

3 Answers 3

5

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.

Sign up to request clarification or add additional context in comments.

Comments

4

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.

Comments

2

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.

3 Comments

What does the requires clauses in this text mean? Isn't concepts a feature of c++20?
optional exists in C++17, so clearly it can be done in C++17. And since 17 isn't that different from 14 in this regard, I suspect it can be done in C++14 too.
@nicol sure? Requires is just one solution. It can just not exist.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.