w1th0utnam3 w1th0utnam3 - 2 months ago 13
C++ Question

Implementing std::variant converting constructor - or: how to find first overload of all conversions from any T to Ti from parameter pack

In the latest working draft (page 572) of the C++ standard the converting constructor of

std::variant
is annotated with:

template <class T> constexpr variant(T&& t) noexcept(see below );



Let Tj be a type that is determined as follows: build an imaginary function FUN (Ti) for each alternative type Ti. The overload FUN (Tj) selected by overload resolution for the expression
FUN (std::forward<T>(t))
defines the alternative Tj which is the type of the contained value after construction.

Effects: Initializes *this to hold the alternative type Tj and
direct-initializes the contained value as if
direct-non-list-initializing it with
std::forward<T>(t)
.

[...]

Remarks: This function shall not participate in overload resolution unless
is_same_v<decay_t<T>, variant>
is false, unless
is_constructible_v<Tj, T>
is true, and unless the expression
FUN ( std::forward<T>(t))
(with FUN being the above-mentioned set of
imaginary functions) is well formed.


On cppreference the following example is used to illustrate the conversion:

variant<string> v("abc"); // OK
variant<string, string> w("abc"); // ill-formed, can't select the alternative to convert to
variant<string, bool> x("abc"); // OK, but chooses bool


How can you mimic the imaginary overload resolution to obtain the final type
Tj
?

Answer

The technique I'll describe is to actually build an overload set, and perform overload resolution by attempting to call it and see what happens with std::result_of.

Building the Overload Set

We define a function object that recursively defines an T operator()(T) const for each T.

template <typename... Ts> struct overload;

template <> struct overload<> { void operator()() const; };

template <typename T, typename... Ts>
struct overload<T, Ts...> : overload<Ts...> {
  using overload<Ts...>::operator();
  T operator()(T) const;
};

// void is a valid variant alternative, but "T operator()(T)" is ill-formed
// when T is void
template <typename... Ts>
struct overload<void, Ts...> : overload<Ts...> {
  using overload<Ts...>::operator();
  void operator()() const;
};

Performing Overload Resolution

We can now use std::result_of_t to simulate overload resolution, and find the winner.

// Find the best match out of `Ts...` with `T` as the argument.
template <typename T, typename... Ts>
using best_match = std::result_of_t<overload<Ts...>(T)>;

Within variant<Ts...>, we would use it like this:

template <typename T, typename U = best_match<T&&, Ts...>>
constexpr variant(T&&);

Some Tests

Alright! Are we done? The following tests pass!

// (1) `variant<string, void> v("abc");` // OK
static_assert(
    std::is_same_v<std::string,
                   best_match<const char*, std::string, void>>);

// (2) `variant<string, string> w("abc");` // ill-formed
static_assert(
    std::is_same_v<std::string,
                   best_match<const char*, std::string, std::string>>);

// (3) `variant<string, bool> x("abc");` // OK, but chooses bool
static_assert(
    std::is_same_v<bool,
                   best_match<const char*, std::string, bool>>);

Well, we don't want (2) to pass, actually. Let's explore a few more cases:

No viable matches

If there are no viable matches, the constructor simply SFINAEs out. We get this behavior for free in best_match, because std::result_of is SFINAE-friendly as of C++14 :D

Unique Match

We want the best match to be a unique best match. This is (2) that we would like to fail. For example, we can test this by checking that the result of best_match appears exactly once in Ts....

template <typename T, typename... Ts>
constexpr size_t count() {
  size_t result = 0;
  constexpr bool matches[] = {std::is_same_v<T, Ts>...};
  for (bool match : matches) {
    if (match) {
      ++result;
    }
  }
  return result;
}

We can then augment this condition onto best_match in a SFINAE-friendly way:

template <typename T, typename... Ts>
using best_match_impl = std::enable_if_t<(count<T, Ts...>() == 1), T>;

template <typename T, typename... Ts>
using best_match = best_match_impl<std::result_of_t<overload<Ts...>(T)>, Ts...>;

Conclusion

(2) now fails, and we can simply use best_match like this:

template <typename T, typename U = best_match<T&&, Ts...>>
constexpr variant(T&&);

More Tests

template <typename> print;  // undefined

template <typename... Ts>
class variant {
  template <typename T, typename U = best_match<T&&, Ts...>>
  constexpr variant(T&&) {
    print<U>{}; // trigger implicit instantiation of undefined template error.
  }
};

// error: implicit instantiation of undefined template
// 'print<std::__1::basic_string<char> >'
variant<std::string> v("abc");

// error: no matching constructor for initialization of
// 'variant<std::string, std::string>'
variant<std::string, std::string> w("abc");

// error: implicit instantiation of undefined template 'print<bool>'
variant<std::string, bool> x("abc");