Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How to use SFINAE to restrict overload to input iterators

Many container class templates have, among others, a constructor that takes a count and a sample element as argument, and another constructor that takes a pair of input iterators. Other methods like insert exhibit the same pattern. A naive implementation will run into trouble when the template is instantiated with an integral type, and the constructor (or other method) is called with a pair of integers: their types being equal, the input iterator method will give a valid argument type deduction, but then fail to compile.

I'm looking for an elegant way to specify that the input iterator method is to participate in overloading only for (equal) argument types that are actually valid input iterator types, or at the very least not to participate for integer types. I have got the impression that SFINAE is the way to go (but would be delighted to be told differently), but when reading about it, frankly I don't quite understand the rules, and moreover the solutions presented in examples hardly qualify as elegant.

As a side restriction, I would like my code to comile with gcc 4.6 which has incomplete C++11 support. Notably I would like to avoid using template aliases. Here's my clumsy attempt (bare bones):

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

And here's a live example. The class template input_iter is trying to do things a bit redundantly: check that the type is not integral and then that it is actually an input iterator. Preferably I'd use just the second part, but that does not work; I get a template instantiation error when trying int for I (no iterator_category), and apparently this is not SFINAE. While I don't understand just why this is so, I added the first part to avoid the error, using the laziness of the and (&&) operator, but apparently to no avail. I can actually get this to compile by removing the second part of the condition, so my real question is not so much to get this to work in some way or another, but to understand what is going on.

One curious thing I noticed is that g++ only gives an error message mentioning the definition of a3. So on one hand, making one argument unsigned apparently avoids trying the iterator overload (even though the other argument can easily be converted to unsigned), and on the other hand repeating the offending definition of a3 for a4 and a5 does not repeat the error message (but if one repairs the a3 definition, surely gcc balks at the a4 definition). Also note that clang++ does not point at any specific definition of one of the a variables, though repairing all of them makes it silent.

What am I doing wrong, and/or should obviously be doing differently?

like image 345
Marc van Leeuwen Avatar asked Sep 04 '14 15:09

Marc van Leeuwen


1 Answers

With help of the comments, and some more experimenting, I will try to formulate an answer myself.

I gather that Substitution Failure is only (SFI)NAE if it occurs in the "immediate context" of the relevant declaration, which is apparently a consecrated (but not very clearly explained) term used in 14.8.2;8 where it says "Only invalid types and expressions in the immediate context of the function type and its template parameter types can result in a deduction failure". Leaving aside that constructor methods don't really have any type, the failure/error in the example arises when trying to construct the defaulted type for the (otherwise unused) second template argument, but substituting a derived integral type for InputIt into the fragment typename std::enable_if...::type. Finding the value of the Boolean (nontype) template argument of std::enable_if is in itself performed in the "immediate context" (of the type substitution); the previous paragraph 14.8.2;7 is clear about this ("expressions include not only constant expressions such as those that appear in array bounds or as nontype template arguments..."). However computing that Boolean value requires forming the class template specialisation input_iter<InputIt>, and that is an auxiliary activity that is no longer performed in the immediate context. It turns out that during that template specialisation another template specialisation is performed, namely std::iterator_traits<InputIt>; this is not in the immediate context either, but this fact is irrelevant here since the latter specialisation never fails. However the resulting structure does not have an appropriately defined iterator_category member when InputIt is deduced to be an integral type, which causes the specialisation input_iter<InputIt> to fail in that case. Not being in the immediate context of the original type substitution, this failure make the program ill-formed.

So the real culprit, the element that separates the failure from the context where SFINEA would apply, is the specialisation input_iter<InputIt>, whereas std::iterator_traits<InputIt> is only indirectly involved. As a consequence, the example can be made to compile by cutting out the type derivation performed by the input_iter template, and feeding the boolean expression directly into enable_if. Since I wanted to avoid using template aliases, this requires (since there is no such thing as boolean-value templates) forgetting input_iter altogether and moving the whole expression it contained into the constructor template declaration, giving

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

The std::is_integral part is not really useful, and can be omitted, leaving just the std::is_base_of part. The resulting code compiles and runs correctly, in fact even with gcc 4.6. This shows that in spite of appearances, one can use std::iterator_traits with SFINAE. When template aliases are available, one of those can be used to lift the enable_if<...>::type part out of the constructor declaration to make it look more appetising, but without them I cannot see how to do it and still correctly invoke SFINAE.

like image 152
Marc van Leeuwen Avatar answered Oct 06 '22 01:10

Marc van Leeuwen