3

I am trying to write a template function similar to std::to_string that works for basic types as well as iterators of STL containers. But I am not sure how to write the templates specific enough to identify just the iterators.

What I tried so far is to try to use the iterator typedef in STL containers

  template<typename... Args, template <typename...> class Container>
  static string to_string(typename Container<Args...>::iterator s) { ...

A minimal example is attached below. The code compiles but the template function My::to_string fails to match the above signature, and treated std::set<int>::iterator as a default type.

My question is how to write this correctly in a generic manner, so that the template function My::to_string can pickup iterators, but do not confuse iterators with other standard template types like std::string.

Thanks in advance.

#include <set>
#include <iostream>
using namespace std;

class My{
  //base case
  template<typename T>
  static string to_string(const T& t) {
    return "basic ";
  }

  //specialization for string
  template <typename Char, typename Traits, typename Alloc>
  static string to_string(const std::basic_string<Char, Traits, Alloc>& s) {
    return (string)s;
  }

  //Problem line: how to write specialization for iterators of standard containers?
  template<typename... Args, template <typename...> class Container>
  static string to_string(typename Container<Args...>::iterator s) {
    return "itor ";
  }
};

int main() {
  int i =  2;
  string str = "Hello";
  set<int> s;
  s.insert(i);
  cout << to_string(i) << ", " << str << ", "
       << to_string(s.begin()) << endl;   //didn't get captured by iterator spec.
}

Output:

basic, Hello, basic

Desired output:

basic, Hello, itor
thor
  • 21,418
  • 31
  • 87
  • 173
  • 4
    The left of `::` is a non-deduced context. Also this probably isn't generally possible. What if two containers both uses a raw pointer as an iterator? – T.C. Jul 20 '14 at 07:01

3 Answers3

4

If you only care about the iterator-ness of the parameter, and not the type of the container, then you can SFINAE out the other overload.

First make an is_iterator trait, as shown in this answer:

template <typename T>
struct sfinae_true : std::true_type {};

struct is_iterator_tester {
    template <typename T>
    static sfinae_true<typename std::iterator_traits<T>::iterator_category> test(int);

    template <typename>
    static std::false_type test(...);
};

template <typename T>
struct is_iterator : decltype(is_iterator_tester::test<T>(0)) {};

Now SFINAE out the wrong overload depending on whether the type is an iterator:

//base case
template<typename T>
static std::enable_if_t<!is_iterator<T>::value, string> to_string(const T& t) {
  return "basic ";
}

//specialization for string
template <typename Char, typename Traits, typename Alloc>
static string to_string(const std::basic_string<Char, Traits, Alloc>& s) {
  return (string)s;
}

//Problem line: how to write specialization for iterators of standard containers?
template<typename T>
static std::enable_if_t<is_iterator<T>::value, string> to_string(const T& s) {
  return "itor ";
}

Demo.

Community
  • 1
  • 1
T.C.
  • 133,968
  • 17
  • 288
  • 421
  • Thanks. I see that your demo used `-std=c++1y`. I tried this with gcc-4.8.1, but it didn't compile, saying `error: 'enable_if_t' in namespace 'std' does not name a type`. Does this require gcc4.9? – thor Jul 20 '14 at 08:57
  • 1
    @TingL replace `std::enable_if_t<...>` with `typename std::enable_if<...>::type` if your compiler doesn't support it. – T.C. Jul 20 '14 at 08:59
  • Thanks, your solution works with `template using enable_if_t = typename std::enable_if::type;` – thor Jul 20 '14 at 15:31
1

This is quite simple if you constrain the iterator-specific overload to work with any type for which operator* is defined (Live at Coliru):

namespace My {
    // base case
    using std::to_string;

    // overload for strings
    template <typename Char, typename Traits, typename Alloc>
    std::basic_string<Char, Traits, Alloc>
    to_string(std::basic_string<Char, Traits, Alloc> s) {
        return s;
    }

    // overload for iterators
    template<typename Iterator>
    auto to_string(Iterator i) -> decltype(to_string(*i)) {
        return "iterator to: \"" + to_string(*i) + '"';
    }
}
Casey
  • 41,449
  • 7
  • 95
  • 125
  • Thanks. This simplifies things. – thor Jul 20 '14 at 16:27
  • Note that this doesn't compile if the type passed can't be passed to `std::to_string` and yet isn't an iterator or string. Also, `to_string('a')` will give you 97 since `std::to_string` doesn't have an overload that takes anything smaller than a `int`. Depending on the use case, this may or may not matter. – T.C. Jul 20 '14 at 17:29
-1

It should be more like this:

template<typename ForwardIterator>
static string to_string(ForwardIterator begin, ForwardIterator end) {
    return "itor ";
}
John Zwinck
  • 239,568
  • 38
  • 324
  • 436
  • 2
    to_string taking two parameters? That's kind of counterintuitive – Mooing Duck Jul 20 '14 at 07:07
  • 1
    @MooingDuck: since all you can do with a single iterator is dereference it, a to_string taking a single iterator seems pointless to me. What would be the benefit of `to_string(it)` over `to_string(*it)` which works already? Perhaps I misunderstood the OP's question but it sounded like the point was to stringify a container. – John Zwinck Jul 20 '14 at 08:37
  • 1
    @MooingDuck On the other hand most functions taking only one iterator are either pointless or wrong. – pmr Jul 20 '14 at 17:18
  • @JohnZwinck: I saw nothing in the OP about stringifying a container, but on the other hand, I can't think of any other possible use, so I guess two iterators does make sense. – Mooing Duck Jul 21 '14 at 16:28