Search code examples
c++c++11templatesc++17sfinae

Template that accepts only iterators pointing to arithmetic types


I'm trying to teach myself the SFINAE pattern and for a thing I'm writing I wanted to write a function that accepts start, end iterators to a value of an arithmetic type (e.g. for summing). This is what I came up with:

My main.cpp:

#include <iostream>
#include <vector>

#include "summer.hpp"


int main()
{
    std::vector<double> vec {0.1, 0.2, 0.3};  // these are OK
    auto sum = summer(vec.begin(), vec.end());
    std::cout << sum << std::endl;
    std::vector<std::string> vec2 {"a", "b"};  // these should be rejected
    auto sum2 = summer(vec2.begin(), vec2.end());
    std::cout << sum2 << std::endl;
    return 0;
}

and then summer.hpp:

#include <type_traits>

template <
    typename Iter,
    typename = typename std::enable_if_t<std::is_arithmetic<Iter>::value_type, Iter>,
    typename T = typename Iter::value_type
>
T summer(Iter start, Iter end)
{
    T sum{};
    for (auto it = start; it != end; it++)
    {
        sum += *it;
    }
    return sum;
}

The SFINAE bit above I took from this answer and just tweaked it to use the type trait that corresponds to the type an iterator points to (value_type). But I'm struggling to compile it, I'm getting a barrage of complaints about value_type being parsed as a non-type but yielding a type and hints that I should prefix it with adding typename (which is wrong):

$ g++ --std=c++17 main.cpp  && ./a.out 
main.cpp: In function ‘int main()’:
main.cpp:10:45: error: no matching function for call to ‘summer(std::vector<double>::iterator, std::vector<double>::iterator)’
   10 |     auto sum = summer(vec.begin(), vec.end());
      |                                             ^
In file included from main.cpp:4:
summer.hpp:8:3: note: candidate: ‘template<class Iter, class, class T> T summer(Iter, Iter)’
    8 | T summer(Iter start, Iter end)
      |   ^~~~~~
summer.hpp:8:3: note:   template argument deduction/substitution failed:
summer.hpp:5:5: error: dependent-name ‘std::is_arithmetic<_Tp>::value_type’ is parsed as a non-type, but instantiation yields a type
    5 |     typename = typename std::enable_if_t<std::is_arithmetic<Iter>::value_type, Iter>,
      |     ^~~~~~~~
summer.hpp:5:5: note: say ‘typename std::is_arithmetic<_Tp>::value_type’ if a type is meant
main.cpp:13:48: error: no matching function for call to ‘summer(std::vector<std::__cxx11::basic_string<char> >::iterator, std::vector<std::__cxx11::basic_string<char> >::iterator)’
   13 |     auto sum2 = summer(vec2.begin(), vec2.end());
      |                                                ^
In file included from main.cpp:4:
summer.hpp:8:3: note: candidate: ‘template<class Iter, class, class T> T summer(Iter, Iter)’
    8 | T summer(Iter start, Iter end)
      |   ^~~~~~
summer.hpp:8:3: note:   template argument deduction/substitution failed:
summer.hpp:5:5: error: dependent-name ‘std::is_arithmetic<_Tp>::value_type’ is parsed as a non-type, but instantiation yields a type
    5 |     typename = typename std::enable_if_t<std::is_arithmetic<Iter>::value_type, Iter>,
      |     ^~~~~~~~
summer.hpp:5:5: note: say ‘typename std::is_arithmetic<_Tp>::value_type’ if a type is meant

I believe I've put typename in the correct places, if I was to get rid of the arithmetic restrictions compiles just fine but then I don't want it to accept e.g. string vectors.

What am I doing wrong?


Solution

  • Okay, where do I start...

    • typename std::enable_if_t<...> is wrong, remove typename. You only need it if there is :: to the right of the template parameter, e.g. in typename std::enable_if<...Iter...>::type.

    • ::value_type is misplaced, it must be right after Iter.

    • ...::value_type needs typename.

    • std::is_arithmetic<...> must be std::is_arithmetic_v<...> OR std::is_arithmetic<...>::value

    • The second template argument in std::enable_if_t doesn't matter in this case, and can be removed.

    So we end up with this code, which at least works:

    template <
        typename Iter,
        typename = std::enable_if_t<std::is_arithmetic_v<typename Iter::value_type>>,
        typename T = typename Iter::value_type
    >
    

    But wait, there is more:

    • typename T = typename Iter::value_type is a misuse of a template parameter, and can be broken by specifying a custom template argument.

    • typename = std::enable_if_t<...> is a weak SFINAE, since the user can circumvent it by providing any template argument. Prefer this form: std::enable_if_t<..., std::nullptr_t> = nullptr, which doesn't have this problem.

    • You should use std::iterator_traits instead of reading ::value_type directly from the iterator, because some iterators (e.g. pointers) don't have it (thanks @NathanPierson).

    • auto it = start; peforms an unnecessary copy. You can operate directly on start.

    So the final version would look like this:

    template <
        typename Iter,
        std::enable_if_t<std::is_arithmetic_v<typename std::iterator_traits<Iter>::value_type>, std::nullptr_t> = nullptr
    >
    typename std::iterator_traits<Iter>::value_type summer(Iter start, Iter end)
    {
        typename std::iterator_traits<Iter>::value_type sum{};
        for (; start != end; start++)
            sum += *start;
        return sum;
    }