Concepts causing compiler bugs in msvc and clang?

Viewed 220

I have the following code:

#include <variant>

template <typename T>
struct S{};

using Var = std::variant<S<int>, S<float>>;

template <typename T>
concept VariantMember = requires(Var var) { std::get<T>(var); };

void foo(VariantMember auto x) {}
void foo(auto x) {}

void bar()
{
    foo(S<int>{});
    foo(S<char>{});
}

MSVC errors with

error C2338: get(variant<Types...>&) requires T to occur exactly once in Types. (N4835 [variant.get]/5)

This only happens if I use two instances of the same template inside the variant S in this case. So first std::get errors unexpectedly, and then the concept doesn't fail but rather errors directly. Interestingly enough if I use std::get outside of a concept, for example:

std::get<S<int>>(Var{ S<int>{} });

the code compiles fine.

Clang just seems to ignore the concept and always picks the first overload of foo in both cases.

godbolt

Are those actually bugs or did I mess up?

3 Answers

First, your S template is noise. Deleting it.

using Var = std::variant<int, float>;

template <typename T>
concept VariantMember = requires(Var var) { std::get<T>(var); };

template <int = 1>
void foo(VariantMember auto x) {}

template <int = 2>
void foo(auto x) {}

void bar()
{
    foo(int{});
    foo(char{});
}

we get the same error. If I comment out the char case, it compiles fine in MSVC.

std::get<char>(Var{}) is not required to be SFINAE friendly. std::get can (and does) hard fail in MSVC here.

With this version, both clang and msvc call the int=1 overload once I remove the char one that is making hard errors.

How do we deal with the hard error? Write a get that doesn't; safe_get:

template<class T, class...Ts>
requires (1 == (std::is_same_v<T, Ts>+...))
T& safe_get( std::variant<Ts...>& var ) 
  return std::get<T>(var);
}
template<class T, class...Ts>
requires (1 == (std::is_same_v<T, Ts>+...))
T const& safe_get( std::variant<Ts...> const& var ) 
  return std::get<T>(var);
}
// ...
template <typename T>
concept VariantMember = requires(Var var) {
  {safe_get<T>(var)};
};

(I add and claim 1, which gives me "matches and only once").

Then we return your S template and everything works.

Yakk explained the issue: std::get mandates that the type appears in the variant. It is not SFINAE-friendly (i.e. it is not a constraint). As such, your VariantMember<T> concept doesn't really check anything: it is either true or ill-formed.

Instead, we have to write the concept differently. With Boost.Mp11, this is a short one-liner (as always):

template <typename T>
concept VariantMember = mp_contains<Var, T>::value;

It's not clear to me if you need to check if T is simply a member of the variant (as I implemented above) or if you need to check if T exists exactly one time in the variant. If the latter, that's just a different algorithm:

template <typename T>
concept VariantMember = mp_count<Var, T>::value == 1;

Similar to Yakk, I came up with my own get. The reason why I was using get in the first place was to find out whether a type T is a member of a std::variant. So I figured I'd just test if I can call std::get on the variant with T.

Anyhow, here is my solution in case someone finds it useful:

namespace detail
{
    template <typename Test, typename Variant>
    struct CountVariantMembers;

    template <typename Test, template <typename ...> typename Variant, typename ...Args>
    struct CountVariantMembers<Test, Variant<Args...>>
    {
        static constexpr std::size_t Count = (std::same_as<Test, Args> + ...);
    };
}

template <typename T, typename Variant>
concept VariantMember = details::CountVariantMembers<T, Variant>::Count > 0;
Related