9

I tried to implement a function which converts a generic type to a string. Integral types need to be converted using std::to_string(), strings and chars using std::string() and vectors, element by element, to a string using one of the other methods (depending on their content).

This is what I have:

//Arithmetic types    

template<class T>
typename std::enable_if<std::is_arithmetic<T>::value, std::string>::type convertToString(const T& t){
    return std::to_string(t);
}

//Other types using string ctor

template<class T>
typename std::enable_if<std::__and_<std::__not_<std::is_arithmetic<T>>::type,
        std::__not_<std::is_same<T, <T,
       std::vector<typename T::value_type, typename T::allocator_type>>::value
       >>>::value, std::string>::type convertToString(const T& t){
    return std::string(t);
}

//Vectors

template<class T>
typename std::enable_if<std::is_same<T, std::vector<typename T::value_type, 
   typename T::allocator_type>>::value, std::string>::type convertToString(const T& t){
    std::string str;
    for(std::size_t i = 0; i < t.size(); i++){
        str += convertToString(t[i]);
    }
    return str;
}

The problem is that the 2nd function does not compile. How can I design the 2nd function so that it does compile (and work) and does not create ambiguity issues?

T.C.
  • 133,968
  • 17
  • 288
  • 421
Overblade
  • 656
  • 2
  • 7
  • 25

3 Answers3

12

Oktalist's answer explains why your type trait doesn't compile. Also, you shouldn't use __and_ and __not_. Those are reserved and could easily change in the next compiler version. It's easy enough to implement your own version of those traits (e.g. see the possible implementation of conjunction).

I would suggest an entirely different approach. We can use choice<> to make overloading these cases far simpler:

template <int I> struct choice : choice<I+1> { };
template <> struct choice<10> { };

Via:

// arithmetic version
template <class T>
auto convertToStringHelper(T const& t, choice<0> )
    -> decltype(std::to_string(t))
{
    return std::to_string(t);
}

// non-arithmetic version
template <class T>
auto convertToStringHelper(T const& t, choice<1> )
    -> decltype(std::string(t))
{
    return std::string(t);
}

// vector version
template <class T, class A>
std::string convertToStringHelper(std::vector<T,A> const& v, choice<2> )
{
    // implementation here
}

template <class T>
std::string convertToString(T const& t) {
    return convertToStringHelper(t, choice<0>{});
}

This is nice because you get all the SFINAE without any of the enable_if cruft.

Community
  • 1
  • 1
Barry
  • 286,269
  • 29
  • 621
  • 977
  • This is a very elegant solution :) I don't like the enable_if syntax very much as is makes code hard to read. – Overblade Jan 08 '17 at 16:26
  • 1
    That's a really good solution; took me a while to figure out how it works, until I realised that it would just slice the `choice<0>` until it reached a matching function. I do believe it has one issue, though: According to the question, he wants `char`s to be processed by the `choice<1>` version, not the `choice<0>` version ([which it currently calls](http://ideone.com/O3N4xB)). – Justin Time - Reinstate Monica Jan 08 '17 at 16:59
  • 2
    @JustinTime `char` is arithmetic, so this matches the OP's ordering. Easy to flip though. – Barry Jan 08 '17 at 17:06
  • @Barry True. Just figured I'd point it out so he could see it now, instead of having to search through his project for a potential bug later. – Justin Time - Reinstate Monica Jan 08 '17 at 17:17
5

One possible way is to add is_vector trait (look here for more details):

template<typename T> struct is_vector : public std::false_type {};

template<typename T, typename A>
struct is_vector<std::vector<T, A>> : public std::true_type {};

And then modify your convertToString function templates as follows:

// Arithmetic types

template<class T>
typename std::enable_if<std::is_arithmetic<T>::value, std::string>::type convertToString(const T& t) {
    return std::to_string(t);
}

// Other types using string ctor

template<class T>
typename std::enable_if<!std::is_arithmetic<T>::value && !is_vector<T>::value, std::string>::type convertToString(const T& t) {
    return std::string(t);
}

// Vectors

template<class T>
typename std::enable_if<!std::is_arithmetic<T>::value && is_vector<T>::value, std::string>::type convertToString(const T& t) {
    std::string str;
    for(std::size_t i = 0; i < t.size(); i++){
        str += convertToString(t[i]);
    }
    return str;
}

wandbox example

Community
  • 1
  • 1
Edgar Rokjān
  • 17,245
  • 4
  • 40
  • 67
3

The template with errors marked:

template<class T>
typename std::enable_if<std::__and_<std::__not_<std::is_arithmetic<T>>::type,
//                                                                    ^^^^^^[1]
        std::__not_<std::is_same<T, <T,
//                                  ^^^[2]
       std::vector<typename T::value_type, typename T::allocator_type>>::value
//                                                                     ^^^^^^^[3]
       >>>::value, std::string>::type convertToString(const T& t){
//       ^[4]
    return std::string(t);
}
// [1] nested ::type not needed and ill-formed without typename keyword
// [2] <T, is garbage
// [3] nested ::value ill-formed because std::__not_ argument must be a type
// [4] too many closing angle brackets

The template with errors fixed:

template<class T>
typename std::enable_if<std::__and_<std::__not_<std::is_arithmetic<T>>,
        std::__not_<std::is_same<T,
       std::vector<typename T::value_type, typename T::allocator_type>>
       >>::value, std::string>::type convertToString(const T& t){
    return std::string(t);
}
Oktalist
  • 14,336
  • 3
  • 43
  • 63