c++c++11variadic-templatesvariadic-functionstemplate-argument-deduction

Variadic template function to create string


I'm new to variadic template functions. I have written a simple class, StringStream, that has a variadic template function that creates a std::string from variable template arguments - strings, ints, etc.

#include <string>
#include <sstream>

class StringStream
{
public:
    StringStream() = default;
    ~StringStream() = default;

    template<typename T>
    std::string Stringify(const T &value)
    {
        mStream << value;
        return mStream.str();
    }

    template<typename T, typename... Ts>
    std::string Stringify(const T& value, Ts... values)
    {
        mStream << value;
        return Stringify(values...);
    }

private:
    std::stringstream mStream;
};

What I want to do now is use a std::string member in StringStream instead of std::stringstream and build the string from the arguments of Stringify(). For arguments that are not std::string I want to convert to strings with std::to_string(), otherwise I just concatenate the argument. I am running into a compiler error. Here's my modified class:

class StringStream
{
public:
    StringStream() = default;
    ~StringStream() = default;

    template<typename T>
    std::string Stringify(const T &value)
    {
        mString += std::to_string(value);
        return mString;
    }

    template<>
    std::string Stringify<std::string>(const std::string& value)
    {
        mString += value;
    }

    template<typename... Ts>
    std::string Stringify(const std::string& value, Ts... values)
    {
        mString += value;
        return Stringify(values...);
    }

    template<typename T, typename... Ts>
    std::string Stringify(const T& value, Ts... values)
    {
        mString += std::to_string(value);
        return Stringify(values...);
    }

private:
    std::string mString;
};

My compiler error says:

error C2665: 'std::to_string': none of the 9 overloads could convert all the argument types

I am calling the function like this:

int main()
{
    int age;
    std::cin >> age;
    StringStream ss;
    std::cout << ss.Stringify("I", " am ", age, " years ", "old") << std::endl;
}

Is there any way to resolve this?


Solution

  • The reason of the error is that, string literals ("I", " am ", " years ", "old") are arrays of constant chars (char const [N], for some N). You can intercept they as char const * but not as std::string.

    A little off topic, I suppose, but I give you two suggestions:

    (1) divide Stringify() in two function: the variadic one, public, that call a private one (toStr(), in my following example) to make conversion over singles arguments

    (2) avoid recursion for the variadic version of Stringify() but simply use pack expansion.

    I mean... you can write Stringify() as follows

      template <typename... Ts>
      std::string Stringify (Ts const & ... vals)
       {
         using unused = int[];
    
         (void)unused { 0, (mString += toStr(vals), 0)... };
    
         return mString;
       }
    

    or, if you can use C++17, using template folding

      template <typename... Ts>
      std::string Stringify (Ts const & ... vals)
       { return ((mString += toStr(vals)), ...); }
    

    For toStr(), I propose a template version that uses std::to_string() but enabled only when the template T type isn't convertible to std::string

      template <typename T>
      typename std::enable_if<
         false == std::is_convertible<T, std::string>::value,
         std::string>::type toStr (T const & val)
       { return std::to_string(val); }
    

    and the non-template version that accept a std::string

      std::string toStr (std::string const & val)
       { return val; }
    

    This way, if an argument is directly convertible to std::string (is std::string or another type that can be used to construct a std::string) the non-template version is called; otherwise is called the template one.

    The following is a full compiling example

    #include <iostream>
    #include <type_traits>
    
    class StringStream
     {
       private:
          std::string mString;
    
          template <typename T>
          typename std::enable_if<
             false == std::is_convertible<T, std::string>::value,
             std::string>::type toStr (T const & val)
           { return std::to_string(val); }
    
          std::string toStr (std::string const & val)
           { return val; }
    
       public:
          StringStream() = default;
          ~StringStream() = default;
    
          template <typename... Ts>
          std::string Stringify (Ts const & ... vals)
           {
             using unused = int[];
    
             (void)unused { 0, (mString += toStr(vals), 0)... };
    
             return mString;
           }
     };
    
    int main ()
     {
       int age = 42;
       StringStream ss;
       std::cout << ss.Stringify("I", " am ", age, " years ", "old") << std::endl;
     }