5

I wrote type traits like classes that can be used test if a given type is "iterable". This is true for arrays (for T[N], not for T[]) and for classes that have a begin and an end method that return things that look like iterators. I wonder if it can be done more concise/simpler than I did it?

Especially the stuff in the impl namespace look a bit roundabout/hacky. It all looks a bit ugly to me. For an example that uses this and can be compiled with g++ and clang++ see: https://gist.github.com/panzi/869728c9879dcd4fffa8

template<typename T>
struct is_iterator {
private:
    template<typename I> static constexpr auto test(void*)
        -> decltype(
            *std::declval<const I>(),
            std::declval<const I>() == std::declval<const I>(),
            std::declval<const I>() != std::declval<const I>(),
            ++ (*std::declval<I*>()),
            (*std::declval<I*>()) ++,
            std::true_type()) { return std::true_type(); }

    template<typename I> static constexpr std::false_type test(...) { return std::false_type(); }

public:
    static constexpr const bool value = std::is_same<decltype(test<T>(0)), std::true_type>::value;
};

namespace impl {
    // implementation details

    template<typename T>
    struct has_iterable_methods {
    private:

        template<typename C> static constexpr auto test(void*)
            -> decltype(
                std::declval<C>().begin(),
                std::declval<C>().end(),
                std::true_type()) { return std::true_type(); }

        template<typename C> static constexpr std::false_type test(...) { return std::false_type(); }

    public:
        static constexpr const bool value = std::is_same<decltype(test<T>(0)), std::true_type>::value;
    };

    template<typename T, bool HasIterableMethods>
    struct returns_iterators : public std::false_type {};

    template<typename T>
    struct returns_iterators<T, true> {
        typedef decltype(std::declval<T>().begin()) begin_type;
        typedef decltype(std::declval<T>().end())   end_type;

        static constexpr const bool value =
            std::is_same<begin_type, end_type>::value &&
            is_iterator<begin_type>::value;
    };
}

template<typename T>
struct is_iterable : public std::integral_constant<
    bool,
    impl::returns_iterators<
        typename std::remove_const<T>::type,
        impl::has_iterable_methods<typename std::remove_const<T>::type>::value>::value> {};

template<typename T, std::size_t N>
struct is_iterable<T[N]> : public std::true_type {};

template<typename T>
struct is_iterable<T*> : public std::false_type {};
panzi
  • 7,517
  • 5
  • 42
  • 54
  • 1
    The [pretty printer](http://stackoverflow.com/q/4850473/596781) has a similar type trait. – Kerrek SB Aug 09 '14 at 23:15
  • `++ (*std::declval())` If you want to create an lvalue, why not just `++ std::declval()`? – dyp Aug 09 '14 at 23:26
  • `begin(std::declval())` may be better than `std::declval().begin()`. – Jarod42 Aug 09 '14 at 23:27
  • 3
    @Jarod42 It seems to me the whole thing could be simplified by the enable-ADL-technique `using std::begin; .. begin(std::declval())`. – dyp Aug 09 '14 at 23:28
  • The definition of function only used in `decltype` may be omitted. – Jarod42 Aug 09 '14 at 23:37
  • 2
    This question belongs on another site in the Stack Exchange network : http://codereview.stackexchange.com/ – Jarod42 Aug 10 '14 at 00:00
  • @Jarod42 Didn't know that site. It seems every time I got back to stackoverflow and post a question a new sub-site spawned where my question would be more appropriate. – panzi Aug 10 '14 at 00:15
  • @dyp I din't use `I&` because I didn't think of it. – panzi Aug 10 '14 at 00:38

1 Answers1

6

First, some boilerplate to do easy argument dependent lookup of begin in a context where std::begin is visible:

#include <utility>
#include <iterator>
namespace adl_details {
  using std::begin; using std::end;
  template<class R>
  decltype(begin(std::declval<R>())) adl_begin(R&&r){
    return begin(std::forward<R>(r));
  }
  template<class R>
  decltype(end(std::declval<R>())) adl_end(R&&r){
    return end(std::forward<R>(r));
  }
}
using adl_details::adl_begin;
using adl_details::adl_end;

This is required to reasonably emulate how range-based for(:) loops find their begin/end iterators. By packaging it up like this, we reduce boilerplate below.

Next, some C++1y style utility aliases:

template<class>struct sink {using type=void;};
template<class X>using sink_t=typename sink<X>::type;
template<bool b, class T=void>using enable_if_t=typename std::enable_if<b,T>::type;

sink_t takes any type, and throws it away replacing it with void.

enable_if_t removes annoying typename spam below.

In an industrial strength library, we'd put this in details, and have a 1-type-argument version that dispatches to it. But I don't care:

template<class I,class=void> struct is_iterator:std::false_type{};
template<> struct is_iterator<void*,void>:std::false_type{};
template<> struct is_iterator<void const*,void>:std::false_type{};
template<> struct is_iterator<void volatile*,void>:std::false_type{};
template<> struct is_iterator<void const volatile*,void>:std::false_type{};
template<class I>struct is_iterator<I,
  sink_t< typename std::iterator_traits<I>::value_type >
>:std::true_type{};

is_iterator doesn't do heavy auditing of the iterator_traits of I. But it is enough.

template<class R>
using begin_t=decltype(adl_begin(std::declval<R&>()));
template<class R>
using end_t=decltype(adl_end(std::declval<R&>()));

These two type aliases make the stuff below less annoying.

Again, in industrial strength libraries, put 2-arg-with-void into details:

template<class R,class=void> struct has_iterator:std::false_type{};
template<class R>
struct has_iterator<
  R,
  enable_if_t<
    is_iterator<begin_t<R>>::value
    && is_iterator<end_t<R>>::value
    // && std::is_same<begin_t<R>,end_t<R>>::value
  >
>:std::true_type{};

Note the commented out line in the enable_if_t above. I left that out to allow asymmetric iteration to work, where the end is a type that has a different operator== overload. Such is being considered for C++17: it allows really, really efficient algorithms on null-terminated strings (for example).

Finally, the final output:

template<class R>using iterator_t=enable_if_t<has_iterator<R>::type, begin_t<R>>;

which evaluates to the iterator of the iterable range R iff it has one.

There are cases where this won't work, but they are pathological.

live example

Yakk - Adam Nevraumont
  • 262,606
  • 27
  • 330
  • 524
  • But you check less requirement than OP. Currently, you only check that `begin(t)` is valid whereas OP check more (pathological) cases (as `begin()` doesn't return iterator type...) – Jarod42 Aug 09 '14 at 23:58
  • Interesting solution. But why the `adl_begin`/`adl_end` function and not directly use `std::begin`/`std::end`? And what does the "adl" stand for? – panzi Aug 10 '14 at 00:36
  • @Jarod42 checks added. – Yakk - Adam Nevraumont Aug 10 '14 at 01:18
  • 1
    @panzi `adl` is "argument dependent lookup". The technique above means that if there is a `begin` or `end` in the same namespace as the type argument, it will be found in preference to `std::begin`, and if not, `std::begin` will be used. This is a pretty decent approximation of how `for(:)` loops find the `begin`/`end` iterators. Simply invoking `std::begin` won't pull that off. – Yakk - Adam Nevraumont Aug 10 '14 at 01:19
  • That doesn't compile if you test `is_iterator::value` or if you test `has_iterator` on a type where the `begin()`/`end()` methods return `void*`. – panzi Aug 10 '14 at 01:40
  • Also it `has_iterator::value` is `false`. `has_iterator::value` is `true`, though. – panzi Aug 10 '14 at 01:46
  • And I also check if begin returns the same type as end. – panzi Aug 10 '14 at 01:47
  • 1
    @panzi two missing `&` added, `has_iterator::value` is now true. (rvalues of type `int[3]` fail to have a valid `begin`, heh). Note that I left `is_same` out on purpose: not only is returning `begin` and `end` with different incompatible iterators a pathological case, if they are compatible that can lead to useful optimizations. Let me see if I can find the article... – Yakk - Adam Nevraumont Aug 10 '14 at 02:28
  • 1
    @panzi [here](http://ericniebler.com/2014/02/21/introducing-iterables/) -- iterables instead of ranges, with non-iterator `end`, may be useful (and coming down the pipe) – Yakk - Adam Nevraumont Aug 10 '14 at 03:09
  • Ok, so your solution says false for `const Foo` where `Foo` only has non-const `begin()`/`end()`. I guess this is right, though. – panzi Aug 10 '14 at 16:21
  • Since we're in C++1y here, could we not use deduced return types to simplify-out the `decltype` usages? Something like: `template auto adl_begin( R &&r ) { using std::begin; return begin(std::forward(r)); }` That avoids the need for the `adl_details` namespace completely, no? – Adam H. Peterson Aug 14 '14 at 18:09
  • @AdamH.Peterson no, implicit `auto` return type deduction does not do SFINAE. Resulting errors are not in the immediate context. Plus, looking forward to C++1y doesn't mean it shouldn't compile in C++11. – Yakk - Adam Nevraumont Aug 14 '14 at 18:34
  • Ah, I didn't realize that the return type is used in the deduction and that implicit return types don't participate. So doing that would result in a hard failure at compile time rather than `has_iterator` correctly specialized? I'll have to read up on that. (I assumed that C++1y was the context, since this question is tagged C++1y and isn't tagged C++11.) – Adam H. Peterson Aug 14 '14 at 22:53