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 namesbegin
andend
each find at least one declaration, then/* begin-expr */
is/* range */.begin()
and/* end-expr */
is/* range */.end()
.- Otherwise,
/* begin-expr */
isbegin(/* range */)
and/* end-expr */
isend(/* range */)
, wherebegin
andend
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.
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>;