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

时间:2014-09-04 15:06:34

标签: c++ templates c++11

许多容器类模板都有一个构造函数,它将count和一个样本元素作为参数,另一个构造函数接受一对输入迭代器。像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
}

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

我注意到的一个奇怪的事情是g++仅提供了一条提及a3定义的错误消息。所以一方面,使一个参数无符号显然避免尝试迭代器重载(即使另一个参数可以很容易地转换为无符号),另一方面重复a3的{​​{1}}的有问题的定义并且a4不会重复出现错误消息(但是如果修复了a5定义,肯定会a3定义gcc。另请注意,a4并未指出其中一个clang++变量的任何特定定义,但修复所有变量会使其保持沉默。

  

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

3 个答案:

答案 0 :(得分:3)

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

我认为替代失败只是(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>;这也不是直接的背景,但这个事实在这里是无关紧要的,因为后者的专业化永远不会失败。但是,当iterator_category被推断为整数类型时,结果结构没有适当定义的InputIt成员,这导致特殊化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部分。生成的代码compiles and runs correctly,实际上甚至是gcc 4.6。这表明,尽管出现了一个可以使用std::iterator_traits与SFINAE。当模板别名可用时,其中一个可用于将enable_if<...>::type部分从构造函数声明中提取出来,使其看起来更加开胃,但如果没有它们,我就看不到如何操作并仍然正确调用SFINAE。 / p>

答案 1 :(得分:2)

一种相当简单直接的方法是:

// 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

答案 2 :(得分:0)

您不需要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>{}}
    {}
};