c++templatesc++17rangetype-traits

Is there a type_traits way to check if a type is compatible with range-for?


Consider a class which uses a templated "iterable" type like this:

template <typename Iterable>
struct foo {
    void bar(Iterable& collection) {
        for (const auto& item : collection) {
            // ...
        }
    }
};

Is there a <type_traits> way to constrain the Iterable type to types which are compatible with the range-for loop?

template <typename Iterable,
    std::enable_if<std::is_range_for_iterable_v<Iterable>> * = nullptr>

Nothing like std::is_range_for_iterable jumps out at me on cppreference's list of type traits. So I looked deeper into what it actually means to be "compatible with range-for":

Exposition-only expressions /* begin-expr */ and /* end-expr */ are defined as follows:

  • If the type of /* range */ is a reference to an array type R:
    • If R is of bound N, /* begin-expr */ is /* range */ and /* end-expr */ is /* range */ + N.
    • If R is an array of unknown bound or an array of incomplete type, the program is ill-formed.
  • If the type of /* range */ is a reference to a class type C, and searches in the scope of C for the names begin and end each find at least one declaration, then /* begin-expr */ is /* range */.begin() and /* end-expr */ is /* range */.end().
  • Otherwise, /* begin-expr */ is begin(/* range */) and /* end-expr */ is end(/* range */), where begin and end are found via argument-dependent lookup (non-ADL lookup is not performed).

From this description I might be able to concoct something, by using is_array for the first case and is_function or is_invocable to look for matching begin() and end() functions for the other two cases. But I'm afraid this would be quite a mess and very likely to suffer from corner cases (rejecting scenarios that are in fact range-for-compatible and vice versa).

Is there a simpler or already-established way?

C++20 concepts offer a much nicer solution to this, but I'm looking for a C++17 solution where type_traits is the best tool available.


Solution

  • In C++20 you can use std::ranges::input_range, which is a concept rather than a trait.

    Pre-C++20 you can throw something together using the detection idiom:

    #include <iterator>
    #include <type_traits>
    #include <utility>
    
    namespace detail
    {
        template <typename ...P> struct void_type {using type = void;};
    
        template <typename DummyVoid, template <typename...> typename A, typename ...B> struct is_detected : std::false_type {};
        template <template <typename...> typename A, typename ...B> struct is_detected<typename void_type<A<B...>>::type, A, B...> : std::true_type {};
    }
    
    template <template <typename...> typename A, typename ...B>
    constexpr bool is_detected = detail::is_detected<void, A, B...>::value;
    
    namespace detail::DetectBeginEnd
    {
        using std::begin;
        using std::end;
    
        template <typename T>
        using Detect = decltype((begin(std::declval<T &>()), end(std::declval<T &>())));
    }
    
    template <typename T>
    constexpr bool is_range = is_detected<detail::DetectBeginEnd::Detect, T>;