8

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?

w1th0utnam3
  • 963
  • 7
  • 19

1 Answers1

10

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 T>
struct identity { using type = 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();
  identity<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();
  identity<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 = typename std::result_of_t<overload<Ts...>(T)>::type;

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");
Tomilov Anatoliy
  • 15,657
  • 10
  • 64
  • 169
mpark
  • 7,574
  • 2
  • 16
  • 18
  • Wow, this is awesome, thanks for the detailed answer! I thought about using recursion but the `using overload::operator();` didn't cross my mind. I didn't know that it is actually possible to introduce *all* recursive overloads this way. I'll accept it as an answer as soon as I'm able to play around with it later today. – w1th0utnam3 Sep 17 '16 at 15:40
  • 4
    `void` is a valid alternative for `variant`, but `T operator()(T);` is ill-formed when `T` is `void`. Easy workaround is to specialize `overload` (which I have applied). Also, the `const` qualifiers on the member function declarations are extraneous - `result_of_t(T)>` is the result of invoking a non-`const` xvalue `overload`. (They are not incorrect, though, so I left them in place.) – Casey Sep 17 '16 at 18:44
  • Small remark: I had to replace the `count` function with a recursive template to make it work in MSVC'15' when compiling with C++14. – w1th0utnam3 Sep 17 '16 at 18:47
  • @Casey: yeah, thanks for pointing that out! I probably should've mentioned things that I omitted. In my implementation I replace `void` with `__variant_void` and just use this as-is rather than specializing for `void`, but yes, certainly it needs to be taken care of one way or another :) – mpark Sep 17 '16 at 19:52
  • 1
    I should also point out that for array types, the `overload` member function declarations accept pointers, which is definitely not right. Given that I don't recall how I handled that problem in my implementation, I strongly suspect I did not ;) – Casey Sep 17 '16 at 20:51
  • 2
    @Casey: As far as I understand, if `T_j` turns out to be an array type and `T` a pointer, then the constructor would be SFINAEd out by the `is_constructible` condition. – mpark Sep 17 '16 at 23:06
  • Yes, thanks - that explains why I don't remember having to deal with the issue. – Casey Sep 18 '16 at 06:15
  • Looking at `T operator () (T) const;` I remember that it can be broken, due to `T` generally cannot be returned from function or can has a deleted destructor as [pointed here](http://stackoverflow.com/questions/35525912/t-declval-instead-of-t-declval-for-common-type#comment58754835_35525912). – Tomilov Anatoliy Sep 19 '16 at 06:51
  • I think it is better to return any reference to `T`, then apply `std::remove_reference_t< decltype() >` to it. – Tomilov Anatoliy Sep 19 '16 at 06:52
  • @Orient: I think you're right. my guess would be that those cases would also produce an ill-formed or at least an unusable `variant` definition, but you're still right. I don't think that your suggested suggestion would work, since `T` can be a reference type, and we wouldn't want to lose the original ref-qualifier via `std::remove_reference`. I think the safer option is to return a `id` where `id` is `template id { using type = T; };` and have `template using best_match = typename std::result_of_t(T)>::type;`. – mpark Sep 19 '16 at 12:12
  • Is variant intended to hold references? I think no, because optional - no.There is reference_wrapper to make it possible. Anyways using identity metafunction is a beautiful way. – Tomilov Anatoliy Sep 19 '16 at 13:31
  • @Orient: Yes, it does allow references. `All types in Types... shall be (possibly cv-qualified) object types, (possibly cv-qualified) void, or references. [ Note: Implementations could decide to store references in a wrapper. —end note ]`. It is indeed different than what `std::optional` does. – mpark Sep 19 '16 at 13:33
  • This gets even harder in C++20 because of [P0608R3 A sane variant converting constructor](http://www.open-std.org/jtc1/sc22/wg21/docs/papers/2018/p0608r3.html). "This paper proposes to constrain the variant converting constructor and the converting assignment operator to prevent narrowing conversions and conversions to bool." – L. F. Jan 11 '20 at 03:50