Search code examples
c++templatesiteratorsfinaeenable-if

std::enable_if or SFINAE for iterator or pointer


I would like to write a constructor for MyClass that take an argument and I want this to compile only if the argument is a pointer or an iterator (something having iterator_traits). How to achieve this ?


Solution

  • Regrettably, there is no standard way to detect whether a class models Iterator. The simplest check would be that *it and ++it are both syntactically valid; you can do this using standard SFINAE techniques:

    template<typename T,
        typename = decltype(*std::declval<T&>(), void(), ++std::declval<T&>(), void())>
        MyClass(T);
    

    Considering the Iterator requirements from 24.2.2:2:

    template<typename T> typename std::enable_if<
        !std::is_void<decltype(*std::declval<T &>())>::value
        && std::is_same<decltype(++std::declval<T &>()),
                        typename std::add_lvalue_reference<T>::type>::value,
        std::true_type>::type has_iterator_requirements_helper(int);
    template<typename T> std::false_type has_iterator_requirements_helper(...);
    template<typename T> struct has_iterator_requirements:
        decltype(has_iterator_requirements_helper<T>(0)) {};
    
    template<typename, bool> struct is_iterator_check: std::false_type {};
    template<typename T> struct is_iterator_check<T, true>: std::true_type {
        typedef typename std::iterator_traits<T>::difference_type difference_type;
        typedef typename std::iterator_traits<T>::value_type value_type;
        typedef typename std::iterator_traits<T>::iterator_category iterator_category;
        typedef typename std::iterator_traits<T>::reference reference;
        typedef typename std::iterator_traits<T>::pointer pointer;
        static_assert(std::is_same<decltype(*std::declval<T &>()), reference>::value
            || std::is_void<reference>::value, "*r must be of type reference");
    };
    template<typename T> struct is_iterator: is_iterator_check<T,
        (std::is_pointer<T>::value
         && !std::is_void<typename std::remove_pointer<T>::type>::value
         && !std::is_function<typename std::remove_pointer<T>::type>::value
         ) || (std::is_copy_constructible<T>::value
         && std::is_copy_assignable<T>::value
         && std::is_nothrow_destructible<T>::value
         // TODO: check lvalues are swappable
         && has_iterator_requirements<T>::value
         )> {};
    

    The problem with trying to use iterator_traits is that it is a template defined for all types, and its instantiation will fail in a non-SFINAE context (recall that SFINAE only applies for direct substitution failure). libstdc++ has a conforming extension whereby instantiating iterator_traits on non-iterator types will produce an empty type; you can do a similar trick by checking for the existence of iterator_category on the type:

    template<typename T> std::true_type has_iterator_category_helper(
        T::iterator_category *);
    template<typename T> std::false_type has_iterator_category_helper(...);
    template<typename T> struct has_iterator_category<T>:
        decltype(has_iterator_category_helper<T>(0)) { };
    template<typename T> struct is_iterator: std::integral_constant<bool,
        std::is_pointer<T>::value || has_iterator_category<T>::value> {};
    
    template<typename T, typename = std::enable_if<is_iterator<T>::value>>
        MyClass(T);
    

    This will however not work for types that do not themselves expose iterator_category but have been adapted by a separate iterator_traits specialisation; in that case the simple SFINAE method makes more sense (and you can instantiate iterator_traits within the constructor to confirm that the type is iterator-like).