如何使用SFINAE来限制输入迭代器的过载

How to use SFINAE to restrict overload to input iterators

本文关键字:迭代器 输入 何使用 SFINAE      更新时间:2023-10-16

许多容器类模板都有一个以计数和样本元素为参数的构造函数,以及另一个以一对输入迭代器为参数的构造器。其他方法如insert也表现出相同的模式。当模板用整数类型实例化,构造函数(或其他方法)用一对整数调用时,天真的实现会遇到麻烦:它们的类型相等,输入迭代器方法会给出有效的参数类型推导,但随后无法编译。

我正在寻找一种优雅的方式来指定输入迭代器方法是只参与实际有效的输入迭代程序类型的(相等)参数类型的重载,或者至少不参与整数类型的重载。我的印象是SFINAE是一条路(但如果能有不同的说法,我会很高兴),但当读到它时,坦率地说,我不太理解规则,而且示例中提供的解决方案很难称得上优雅。

作为一个附带的限制,我希望我的代码与gcc 4.6兼容,它具有不完整的C++11支持。值得注意的是,我希望避免使用模板别名。这是我笨拙的尝试(赤裸裸的):

#include <iterator>
#include <type_traits>
#include <vector>
template <typename I>
  struct input_iter : public std::integral_constant<bool,
  not std::is_integral<I>::value
  and std::is_base_of
    <std::input_iterator_tag
    ,typename std::iterator_traits<I>::iterator_category
    >::value >
  {};
template<typename T>
  struct container
{
  container (size_t count, const T& init);
  template <typename InputIt,
            typename = typename std::enable_if<input_iter<InputIt>::value>::type >
    container (InputIt first,InputIt last);
};
typedef container<int> int_cont;
void f()
{ std::vector<int> v (5,3);
  int_cont a1 (3u,6); // first constructor
  int_cont a2 (3u,6); // first constructor
  int_cont a3 (3,6); // first constructor
  int_cont a4 (3,6); // first constructor
  int_cont a5 (3,6); // first constructor
  int_cont b(v.begin(),v.end()); // second constructor
}

这是一个活生生的例子。类模板input_iter试图做一些多余的事情:检查类型是否不是整数,然后检查它是否实际上是一个输入迭代器。我最好只使用第二部分,但这不起作用;我在为I尝试int时遇到了一个模板实例化错误(没有iterator_category),显然这不是SFINAE。虽然我不明白为什么会这样,但为了避免错误,我添加了第一部分,使用了and&&)运算符的惰性,但显然没有用。实际上,我可以通过删除条件的第二部分来编译它,所以我真正的问题不是让它以某种方式工作,而是了解发生了什么

我注意到的一件奇怪的事情是,g++只给出了一条错误消息,提到了a3的定义。因此,一方面,使一个参数无符号显然避免了尝试迭代器重载(即使另一个参数可以很容易地转换为无符号),另一方面,对a4a5重复a3的错误定义不会重复错误消息(但如果修复a3定义,gcc肯定会拒绝a4定义)。还要注意的是,clang++并不指向其中一个a变量的任何特定定义,尽管修复所有这些变量会使其静音。

我做错了什么,和/或显然应该采取不同的做法?

在评论和更多实验的帮助下,我将尝试自己制定一个答案。

我认为,只有在相关声明的"直接上下文"中发生的替代失败才是(SFI)NAE,这显然是14.8.2中使用的一个神圣的(但解释不太清楚)术语;8,其中它说"只有函数类型及其模板参数类型的直接上下文中的无效类型和表达式才能导致推导失败"。撇开构造函数方法实际上没有任何类型不谈,当试图为(否则未使用的)第二个模板参数构造默认类型,但将InputIt的派生积分类型替换为片段typename std::enable_if...::type时,就会出现示例中的失败/错误。查找std::enable_if的布尔(非类型)模板参数的值本身是在(类型替换的)"即时上下文"中执行的;前14.8.2段;7对此很清楚("表达式不仅包括常量表达式,如出现在数组边界中或作为非类型模板参数的表达式…")。然而,计算布尔值需要形成类模板专用input_iter<InputIt>,这是一种辅助活动,不再在直接上下文中执行。事实证明,在模板专业化过程中,会进行另一个模板专业化,即std::iterator_traits<InputIt>;这也不是直接的背景,但这一事实在这里无关紧要,因为后一种专业化从未失败。然而,当InputIt被推断为积分型时,所得结构没有适当定义的iterator_category成员,这导致专门化input_iter<InputIt>在这种情况下失败。由于不在原始类型替换的直接上下文中,这种失败会使程序格式错误。

因此,真正的罪魁祸首,将失败与SFINEA适用的环境区分开来的因素,是专业化input_iter<InputIt>,而std::iterator_traits<InputIt>只是间接参与其中。因此,可以通过剪切input_iter模板执行的类型推导,并将布尔表达式直接输入enable_if来编译该示例。由于我想避免使用模板别名,这需要(因为没有布尔值模板)完全忘记input_iter,并将其包含的整个表达式移动到构造函数模板声明中,从而给出

template <typename InputIt,
          typename = typename std::enable_if<not std::is_integral<InputIt>::value
and std::is_base_of
  <std::input_iterator_tag
  ,typename std::iterator_traits<InputIt>::iterator_category
  >::value>::type >
  container (InputIt first,InputIt last);

std::is_integral部分并不是真正有用的,可以省略,只留下std::is_base_of部分。生成的代码可以正确编译和运行,事实上即使使用gcc 4.6也是如此。这表明,尽管存在外观问题,一个可以std::iterator_traits与SFINAE一起使用。当模板别名可用时,其中一个别名可以用于将enable_if<...>::type部分从构造函数声明中移除,使其看起来更有趣,但如果没有它们,我就看不到如何做到这一点,并且仍然正确地调用SFINAE。

一种相当简单明了的方法是:

// this wont collide
void assign( size_t, const T& );
template<
          typename It,
          typename = decltype(*std::declval<It&>(), ++std::declval<It&>(), void())
        >
void assign(It, It); // SFINAE on *it and ++it

可能不足以提交标准,但
我在生产中使用过它,但它还没有咬我。

如果您的编译器支持表达式sfinae,那么就有一个不那么笨拙的做同样事情的方法:

template<typename It>
auto assign(It b, It e)
    -> decltype(*b, ++b, void())
{
}

对于尾随返回类型,您不必使用std::declval

您不需要SFINAE。我会用is_integral和委派构造函数来完成这项工作。

template<typename T>
struct container
{
public:
    container(std::size_t, const T &t);
private:
    template<typename I>
    // first is integral, delegate to size_t constructor
    container(I first, I last, std::true_type)
      : container{static_cast<std::size_t>(first), last}
    {}
    // first is not integral, assume we have iterators here
    template<typename I>
    container(I first, I last, std::false_type);
public:
    template<typename I>
    container(I first, I last)
      : container{first, last, std::is_integral<I>{}}
    {}
};