jignatius
jignatius

Reputation: 6504

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?

Upvotes: 5

Views: 3955

Answers (3)

c z
c z

Reputation: 9045

You should continue to use stringstream, concatenating strings using + is inefficient, since you copy the entire string for every + operation.

Just convert the result to a string before your function exits. You don't need an instance of a class.:

MWE:

#include <iostream>
#include <sstream>

template<typename... TArgs>
std::string Stringify(const TArgs&... args) {
    std::stringstream ss;
    (ss << ... << args);
    return ss.str();
}

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

Upvotes: 2

max66
max66

Reputation: 66230

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

Upvotes: 3

rafix07
rafix07

Reputation: 20969

You are calling to_string inside

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

so you can remove to_string from Stringify(const T& value, Ts... values), and replace it by just Stringify:

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

add also specialization for const char*:

std::string Stringify(const char* value)
{
    return mString += value;
}

Live demo

Now your problem is that const char[2] is passed to_string, but to_string doesn't have overload which accepts that input. By replacing to_string by Stringify an appropriate overload can be used for first argument from arguments pack.

Upvotes: 1

Related Questions