In the latest working draft (page 572) of the C++ standard the converting constructor of std::variant
is annotated with:
template co
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
.
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;
};
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&&);
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:
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
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...>;
(2)
now fails, and we can simply use best_match
like this:
template <typename T, typename U = best_match<T&&, Ts...>>
constexpr variant(T&&);
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");