【问题标题】:Template that accepts only iterators pointing to arithmetic types只接受指向算术类型的迭代器的模板
【发布时间】:2021-09-03 11:12:31
【问题描述】:

我正在尝试自学 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 向量。

我做错了什么?

【问题讨论】:

  • 那里似乎有一条说明告诉你如何修复它。
  • 请注意,Iter 可能并不总是直接有一个::value_type——毕竟指针是一个迭代器。你真正想要的是std::iterator_traits&lt;Iter&gt;::value_type。您还想检查此值类型是否为算术类型,因为目前您正在检查Iter 本身是否为算术类型。
  • @1201ProgramAlarm 你指的是note: say ‘typename std::is_arithmetic&lt;_Tp&gt;::value_type’ if a type is meant吗?那是行不通的,不是吗?这应该返回bool,而不是typename
  • 为了满足我自己的好奇心:一个使用概念的C++20 version。相当漂亮。
  • @NathanPierson 看起来很酷,为什么不把它作为答案呢?

标签: c++ c++11 templates c++17 sfinae


【解决方案1】:

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

  • typename std::enable_if_t&lt;...&gt; 错误,删除typename。仅当模板参数右侧有 :: 时才需要它,例如在typename std::enable_if&lt;...Iter...&gt;::type

  • ::value_type 放错了位置,它必须在Iter 之后。

  • ...::value_type 需要typename

  • std::is_arithmetic&lt;...&gt; 必须是 std::is_arithmetic_v&lt;...&gt;std::is_arithmetic&lt;...&gt;::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&lt;...&gt; 是一个弱 SFINAE,因为用户可以通过提供任何模板参数来规避它。首选这种形式:std::enable_if_t&lt;..., std::nullptr_t&gt; = nullptr,没有这个问题。

  • 您应该使用std::iterator_traits 而不是直接从迭代器中读取::value_type,因为某些迭代器(例如指针)没有它(thanks @NathanPierson)。

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

【讨论】:

  • 非类型模板参数的替代方法是将summer 的返回类型设为std::enable_if_t&lt;...&gt;。尽管之前可能使用using 声明以避免重复std::iterator_traits&lt;Iter&gt;::value_type 过于笨拙。
  • 感谢您的详细回答,尤其是第二部分,因为我认为仅按照文档,尤其是您的第一个和第二个要点,我不会了解这些细微差别,你能指点我关于它们的资源吗(比如它们为什么工作或不工作)?
  • @Nobilis 没有比我已经做过的更多解释了,所以我没有任何链接。如果用户在显式指定所有 3 个模板参数时调用您的函数,T 可能是错误类型,并且第二个参数的默认参数中的 SFINAE 将被完全忽略。后者是通过将 SFINAE 从默认 arg 移动到参数类型来解决的,在这种情况下不会被忽略。
  • 我使用std::nullptr_t 作为类型,因为它只有一个可能的值,所以用户不能生成不必要的函数实例化(在其中复制静态变量等)传递不同的参数。
  • @Nobilis 您的代码为第二个和第三个模板参数提供了默认值,但没有什么能阻止人们使用非默认值调用它。所以summer&lt;std::string, void, std::string&gt;( ... ) 仍然可以编译,因为通过为模板参数提供你自己的值,你绕过了 SFINAE。
【解决方案2】:

您需要在std::is_arithmetic&lt;...&gt;::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?),而不是在取消引用时Iterator 引用的类型。

您可以简单地在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 迭代器所做的那样),但它不适用于普通的 vanilla 指针,它们也是有效的迭代器。要解决这个问题,您可以改用std::iterator_traits&lt;Iter&gt;::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;

【讨论】:

  • 谢谢你的详细回答,只是一个简单的问题,我想enable_if,第一个参数应该返回一个bool,我确实尝试在std::is_arithmetic前加上typename但它不会编译,因为它抱怨类型/值不匹配(预期为bool,得到typename)。它应该去别的地方吗?
  • accumulate 点是完全公平的,这只是我想出的一个人为的例子来轻松展示我的问题。
  • 啊,我想我明白你的意思了,请忽略我的第一条评论
  • 哦,还有你关于不使用 is_arithmetic_v 的观点,这是 ::value 的别名,而我认为我需要使用 ::value_type,所以我认为它不正确。
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 2021-12-26
  • 1970-01-01
  • 2012-07-20
  • 1970-01-01
  • 2012-07-01
  • 2012-07-02
  • 1970-01-01
相关资源
最近更新 更多