Search code examples
c++templatesc++-conceptsc++23

Is it possible to use `std::index_sequence` in a concept?


Say you wanted to define a concept that checks that all alternatives of a variant are convertible to int. You could do it like so:

#include <concepts>
#include <type_traits>
#include <utility>
#include <variant>

template <typename T>
concept ConvertibleToInt = std::convertible_to<T, int>;

template <typename T>
concept AllAlternativesConvertibleToInt = []<auto... i>(std::index_sequence<i...>){
    return (ConvertibleToInt<std::variant_alternative_t<i, T>> && ...);
}(std::make_index_sequence<std::variant_size_v<T>>{});

If the constraint fails, you get a relatively descriptive error (for C++ standards):

static_assert(AllAlternativesConvertibleToInt<std::variant<char, int, float, double, std::nullptr_t>>);
<source>:12:15: error: static assertion failed
   12 | static_assert(AllAlternativesConvertibleToInt<std::variant<char, int, float, double, std::nullptr_t>>);
      |               ^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
<source>:12:15: note: because 'std::variant<char, int, float, double, std::nullptr_t>' (aka 'variant<char, int, float, double, std::nullptr_t>') does not satisfy 'AllAlternativesConvertibleToInt'
<source>:8:43: note: because '[]<auto ...i>(std::index_sequence<i...>) {
    return (ConvertibleToInt<std::variant_alternative_t<i, variant<char, int, float, double, std::nullptr_t> > > && ...);
}(std::make_index_sequence<std::variant_size_v<variant<char, int, float, double, std::nullptr_t> > >{})' evaluated to false
    8 | concept AllAlternativesConvertibleToInt = []<auto... i>(std::index_sequence<i...>){
      |                                           ^
1 error generated.

However, the error message doesn't show which of the alternatives failed the constraint. For that to happen, AFAIK you'd need to use only concepts.

I was unable to find a way to have a parameter pack from 0 to std::variant_size_v, other than writing it manually like so:

template <typename T>
concept ConvertibleToInt = std::convertible_to<T, int>;

template <typename T, auto... i>
concept AllAlternativesConvertibleToIntImpl = (ConvertibleToInt<std::variant_alternative_t<i, T>> && ...);

template <typename T>
concept AllAlternativesConvertibleToInt = AllAlternativesConvertibleToIntImpl<T, 0, 1, 2, 3, 4>;

static_assert(AllAlternativesConvertibleToInt<std::variant<char, int, float, double, std::nullptr_t>>);

Note that now the error message says alternative #4 does not satisfy the constraint:

<source>:24:15: error: static assertion failed
   24 | static_assert(AllAlternativesConvertibleToInt<std::variant<char, int, float, double, std::nullptr_t>>);
      |               ^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
<source>:24:15: note: because 'std::variant<char, int, float, double, std::nullptr_t>' (aka 'variant<char, int, float, double, std::nullptr_t>') does not satisfy 'AllAlternativesConvertibleToInt'
<source>:22:43: note: because 'AllAlternativesConvertibleToIntImpl<std::variant<char, int, float, double, std::nullptr_t>, 0, 1, 2, 3, 4>' evaluated to false
   22 | concept AllAlternativesConvertibleToInt = AllAlternativesConvertibleToIntImpl<T, 0, 1, 2, 3, 4>;
      |                                           ^
<source>:19:48: note: because 'std::variant_alternative_t<4, variant<char, int, float, double, std::nullptr_t>>' (aka 'std::nullptr_t') does not satisfy 'ConvertibleToInt'
   19 | concept AllAlternativesConvertibleToIntImpl = (ConvertibleToInt<std::variant_alternative_t<i, T>> && ...);
      |                                                ^
<source>:16:28: note: because 'std::convertible_to<std::nullptr_t, int>' evaluated to false
   16 | concept ConvertibleToInt = std::convertible_to<T, int>;
      |                            ^
/opt/compiler-explorer/gcc-14.2.0/lib/gcc/x86_64-linux-gnu/14.2.0/../../../../include/c++/14.2.0/concepts:79:30: note: because 'is_convertible_v<std::nullptr_t, int>' evaluated to false
   79 |     concept convertible_to = is_convertible_v<_From, _To>
      |                              ^
1 error generated.

Is it possible to do this without writing the parameter pack manually?


Solution

  • Weijun Zhou's comment made me realize you can use a function to check which of the alternatives fails the constraint (if any).

    Based on that, I came up with this:

    #include <algorithm>
    #include <variant>
    
    template <typename T>
    concept ConvertibleToInt = std::convertible_to<T, int>;
    
    template <typename T>
    consteval auto get_failing_index()
    {
        return []<auto... i>(std::index_sequence<i...>){
            return std::min({ -1uz, (!ConvertibleToInt<std::variant_alternative_t<i, T>> ? i : -1uz)... });
        }(std::make_index_sequence<std::variant_size_v<T>>{});
    }
    
    template <auto i, typename T, typename Default>
    struct GuardedVariantAlternativeHelper
    {
        using Type = std::variant_alternative_t<i, T>;
    };
    
    template <typename T, typename Default>
    struct GuardedVariantAlternativeHelper<-1uz, T, Default>
    {
        using Type = Default;
    };
    
    template <auto i, typename T, typename Default>
    using GuardedVariantAlternative = GuardedVariantAlternativeHelper<i, T, Default>::Type;
    
    template <typename T, std::size_t i>
    concept AlternativeConvertibleToInt = ConvertibleToInt<GuardedVariantAlternative<i, T, int>>;
    
    template <typename T>
    concept AllAlternativesConvertibleToInt = AlternativeConvertibleToInt<T, get_failing_index<T>()>;
    
    static_assert(AllAlternativesConvertibleToInt<std::variant<>>);
    static_assert(AllAlternativesConvertibleToInt<std::variant<char, int, float, double>>);
    static_assert(AllAlternativesConvertibleToInt<std::variant<char, int, float, double, std::nullptr_t>>);
    

    Which results in:

    <source>:38:15: error: static assertion failed
       38 | static_assert(AllAlternativesConvertibleToInt<std::variant<char, int, float, double, std::nullptr_t>>);
          |               ^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
    <source>:38:15: note: because 'std::variant<char, int, float, double, std::nullptr_t>' (aka 'variant<char, int, float, double, std::nullptr_t>') does not satisfy 'AllAlternativesConvertibleToInt'
    <source>:34:43: note: because 'AlternativeConvertibleToInt<std::variant<char, int, float, double, std::nullptr_t>, get_failing_index<std::variant<char, int, float, double, std::nullptr_t> >()>' evaluated to false
       34 | concept AllAlternativesConvertibleToInt = AlternativeConvertibleToInt<T, get_failing_index<T>()>;
          |                                           ^
    <source>:31:39: note: because 'GuardedVariantAlternative<4UL, variant<char, int, float, double, std::nullptr_t>, int>' (aka 'std::nullptr_t') does not satisfy 'ConvertibleToInt'
       31 | concept AlternativeConvertibleToInt = ConvertibleToInt<GuardedVariantAlternative<i, T, int>>;
          |                                       ^
    <source>:5:28: note: because 'std::convertible_to<std::nullptr_t, int>' evaluated to false
        5 | concept ConvertibleToInt = std::convertible_to<T, int>;
          |                            ^
    /opt/compiler-explorer/gcc-14.2.0/lib/gcc/x86_64-linux-gnu/14.2.0/../../../../include/c++/14.2.0/concepts:79:30: note: because 'is_convertible_v<std::nullptr_t, int>' evaluated to false
       79 |     concept convertible_to = is_convertible_v<_From, _To>
          |                              ^
    1 error generated.
    

    And is pretty much what I wanted.