1

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?

Nobilis
  • 7,310
  • 1
  • 33
  • 67
  • There seems to be a note in there that tells you how to fix it. – 1201ProgramAlarm Jun 18 '21 at 18:22
  • 2
    Note that `Iter` might not always have a `::value_type` directly -- a pointer is an iterator, after all. What you actually want is `std::iterator_traits::value_type`. You also want to check that this value type is arithmetic, because currently you're checking to see if `Iter` itself is an arithmetic type. – Nathan Pierson Jun 18 '21 at 18:26
  • @1201ProgramAlarm Are you referring to `note: say ‘typename std::is_arithmetic<_Tp>::value_type’ if a type is meant`? That won't work, will it? That's supposed to return a `bool`, not a `typename`. – Nobilis Jun 18 '21 at 18:59
  • 2
    To satisfy my own curiosity: A [C++20 version](https://godbolt.org/z/joh9fWjc7) that uses concepts. Quite nifty. – Nathan Pierson Jun 18 '21 at 19:55
  • @NathanPierson That looks cool, why not leave it as an answer? – Nobilis Jun 18 '21 at 19:58
  • Well, it's _not_ an answer--your question is how to get the SFINAE version working, and this is tangential to that. – Nathan Pierson Jun 18 '21 at 20:07
  • I suppose my premise was to ultimately have a template that rejects certain specialisations and I used SFINAE to do it. Reading up on concepts, it seems they partially exist as a more elegant way to do what SFINAE does. – Nobilis Jun 18 '21 at 20:15

2 Answers2

4

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;
}
HolyBlackCat
  • 78,603
  • 9
  • 131
  • 207
  • 2
    An alternate to the non-type template parameter would be to make the return type of `summer` an `std::enable_if_t<...>`. Although probably with a `using` statement earlier to avoid making the repetitions of `std::iterator_traits::value_type` too unwieldy. – Nathan Pierson Jun 18 '21 at 19:12
  • Thank you for the detailed answer, especially the second portion of it since I don't think I would have picked up on those nuances by just following the docs, your first and second bullet points in particular, can you please point me to a resource on them (as in why they work or don't)? – Nobilis Jun 18 '21 at 19:17
  • @Nobilis First and second bullets in the first part of the answer, or in the second one? – HolyBlackCat Jun 18 '21 at 19:20
  • @HolyBlackCat sorry, in the second part, specifically why it's a misuse of a template parameter and also why it's a weak pattern, I think I saw both of those in the docs or at least well voted answers on SO (but perhaps out of date?). – Nobilis Jun 18 '21 at 19:25
  • 1
    @Nobilis There isn't much more to explain than what I already did, so I don't have any links. If user calls your function while explicitly specifying all 3 template arguments, `T` might be a wrong type, and SFINAE in the default argument of the 2nd parameter will be ignored completely. The latter is solved by moving SFINAE from default arg into the parameter type, which isn't ignored in that scenario. – HolyBlackCat Jun 18 '21 at 19:29
  • 1
    I'm using `std::nullptr_t` as the type, since there's only one possible value for it, so the user can't generate unnecessary instantiations of your function (duplicating static variables in it, etc) by passing different arguments it. – HolyBlackCat Jun 18 '21 at 19:29
  • 1
    @Nobilis Your code provides _default values_ for the second and third template parameters, but there's nothing stopping someone from calling it with non-default values. So `summer( ... )` would still compile, because by providing your own values for the template arguments you're bypassing the SFINAE. – Nathan Pierson Jun 18 '21 at 19:30
  • @HolyBlackCat Ah, I see, I didn't realise in `enable_if`, the second parameter is redundant in this case, that makes sense now. – Nobilis Jun 18 '21 at 19:34
2

You need to prefix std::is_arithmetic<...>::value_type with typename since ... depends on another template argument. Just like you did with typename Iter::value_type. See Where and why do I have to put the "template" and "typename" keywords?

That being said, you are applying the std::is_arithmetic check (why not std::is_arithmetic_v?) on the Iterator itself, not on the type that the Iterator refers to when dereferenced.

You could simply use typename Iter::value_type inside of std::is_arithmetic, eg:

#include <type_traits>

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

This will work only for iterator types that define a value_type member (like std::vector iterators do), but it will not work on plain vanilla pointers instead, which are also valid iterators. To work around that, you can use std::iterator_traits<Iter>::value_type instead, eg:

#include <type_traits>

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

Demo

On a side note, your function is largely redundant, since the standard library has a std::accumulate() function that performs the exact same summing action that you are, just without the SFINAE type check as it works on any type that defines an operator+ (which std::string does), eg:

std::vector<double> vec {0.1, 0.2, 0.3};  // these are OK
auto sum = std::accumulate(vec.begin(), vec.end(), 0);
std::cout << sum << std::endl;

std::vector<std::string> vec2 {"a", "b"};  // these are also OK
auto sum2 = std::accumulate(vec2.begin(), vec2.end(), "");
std::cout << sum2 << std::endl;
Remy Lebeau
  • 555,201
  • 31
  • 458
  • 770
  • Thank you for the detailed answer, just a quick question, I thought for `enable_if`, the first argument is supposed to return a `bool`, I did try prefixing `std::is_arithmetic` with `typename` but it won't compile as it complains about a type/value mismatch (expected `bool`, got `typename`). Is it supposed to go somewhere else? – Nobilis Jun 18 '21 at 19:04
  • The `accumulate` point is totally fair, this was just a contrived example that I came up with to easily demonstrate my issue. – Nobilis Jun 18 '21 at 19:05
  • ah, I think I see what you mean, please ignore my first comment – Nobilis Jun 18 '21 at 19:15
  • Oh, and your point about not using `is_arithmetic_v`, that's an alias for `::value` whereas I thought I need to use `::value_type` so I assumed it's just not correct. – Nobilis Jun 18 '21 at 19:21