只接受指向算术类型的迭代器的模板

Template that accepts only iterators pointing to arithmetic types

我正在尝试自学 SFINAE 模式,对于我正在写的东西,我想编写一个接受 startend 迭代器到算术类型值的函数(例如求和)。这是我想出的:

我的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;
}

然后 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;
}

上面的 SFINAE 位是我从 this answer 中获取的,只是对其进行了调整以使用与迭代器指向的类型 (value_type) 相对应的类型特征。但是我正在努力编译它,我收到了一连串关于 value_type 被解析为非类型但产生类型的抱怨,并提示我应该在它前面加上前缀 typename (这错了):

$ 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

我相信我已经把 typename 放在了正确的位置,如果我要摆脱算术限制编译就好了,但我不希望它接受例如string 向量。

我做错了什么?

您需要在 std::is_arithmetic<...>::value_type 前加上 typename 前缀,因为 ... 取决于另一个模板参数。就像您对 typename Iter::value_type 所做的那样。参见 Where and why do I have to put the "template" and "typename" keywords?

话虽如此,您正在对 Iterator 本身应用 std::is_arithmetic 检查(为什么不 std::is_arithmetic_v?),而不是 Iter ator 在取消引用时引用。

您可以简单地在 std::is_arithmetic 中使用 typename Iter::value_type,例如:

#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;
}

这仅适用于定义了 value_type 成员的迭代器类型(就像 std::vector 迭代器一样),但它不适用于普通指针,它们也是有效的迭代器。要解决此问题,您可以改用 std::iterator_traits<Iter>::value_type,例如:

#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

附带说明一下,你的函数在很大程度上是多余的,因为标准库有一个 std::accumulate() 函数,它执行与你完全相同的求和操作,只是没有 SFINAE 类型检查,因为它适用于任何定义 operator+ 的类型(std::string 有),例如:

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;

好的,我从哪里开始...

  • typename std::enable_if_t<...>错误,去掉typename。仅当模板参数右侧有 :: 时才需要它,例如在 typename std::enable_if<...Iter...>::type.

  • ::value_type放错了,一定是在Iter.

    之后
  • ...::value_type 需要 typename.

  • std::is_arithmetic<...> 必须是 std::is_arithmetic_v<...>std::is_arithmetic<...>::value

  • std::enable_if_t 中的第二个模板参数在这种情况下无关紧要,可以删除。

所以我们最终得到了这段代码,它至少可以工作:

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

但是等等,还有更多:

  • typename T = typename Iter::value_type 是对模板参数的误用,可以通过指定自定义模板参数来解决。

  • typename = std::enable_if_t<...> 是一个弱 SFINAE,因为用户可以通过提供任何模板参数来规避它。更喜欢这种形式:std::enable_if_t<..., std::nullptr_t> = nullptr,没有这个问题。

  • 你应该使用std::iterator_traits而不是直接从迭代器读取::value_type,因为一些迭代器(例如指针)没有它() .

  • auto it = start; 执行不必要的复制。可以直接在start.

    上操作

所以最终版本应该是这样的:

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;
}