18

I am attempting to write a function that takes a variety of strings or numbers (that work with std::to_string and concatenate them. I've got it working with just strings, but I am having trouble with specializing depending on input as string or number.

My code is called like this:

stringer("hello", "world", 2, 15, 0.2014, "goodbye", "world")

And here is what I've got:

inline std::string stringer(const std::string &string)
{
    return string;
}

template <typename T, typename... Args>
inline std::string stringer(const std::string &string, T &&val, Args &&...args)
{  
    return stringer(string+std::to_string(val), std::forward<Args>(args)...);
}

template <typename... Args>
inline std::string stringer(const std::string &string, Args &&...args)
{
    return stringer(string, std::forward<Args>(args)...);
}

Currently it is breaking on any more than one string added unless the following are all numbers (due to the to_string). How can I specialize based on string or number to make the above work? Thanks.

jett
  • 1,276
  • 2
  • 14
  • 34

6 Answers6

17
inline std::string const& to_string(std::string const& s) { return s; }

template<typename... Args>
std::string stringer(Args const&... args)
{
    std::string result;
    using ::to_string;
    using std::to_string;
    int unpack[]{0, (result += to_string(args), 0)...};
    static_cast<void>(unpack);
    return result;
}
Simple
  • 13,992
  • 2
  • 47
  • 47
  • Thanks, but I receive error: call of overloaded ‘to_string’ on gcc, and errors on clang as well – jett Feb 16 '14 at 02:49
  • 1
    [Works like a charm once you let the custom to_string be visible](http://ideone.com/gGZhPd). I was going to post my solution that's what you actually asked (specialize on whether `to_string(x)` succeeds) but this is much shorter and elegant. – DanielKO Feb 16 '14 at 04:23
  • @DanielKO Thanks, d'oh. Would you mind posting your solution just for completeness? While this answers my goal I could still learn a thing from yours – jett Feb 16 '14 at 04:38
  • @simple what would the static_cast be doing here? – jett Feb 16 '14 at 04:44
  • 2
    @jett it avoids "unused variable" warnings. – DanielKO Feb 16 '14 at 04:53
  • I love Simple answers =) – oblitum Feb 16 '14 at 12:45
  • For an even simpler answer: in the case of many compilers (e.g gcc + clang) you can additionally ditch the `static_cast` line if you define `unpack` with an unused attribute, like this: `int unpack[] __attribute__((unused)) {0, (out += to_string(args), 0)...};` – fish2000 Apr 17 '15 at 01:44
  • Is `int unpack[]` actually constructed every time or compiler is smart enough to only leave side effects? It's a shame that you can't just do (expression involving pack)... that'd just translate to (expression with first param), (expression with second param), etc. – Dan M. Jan 06 '16 at 17:38
17

Why do not use simple std::stringstream ?

#include <iostream>
#include <string>
#include <sstream>

template< typename ... Args >
std::string stringer(Args const& ... args )
{
    std::ostringstream stream;
    using List= int[];
    (void)List{0, ( (void)(stream << args), 0 ) ... };

    return stream.str();
}

int main()
{
    auto s = stringer("hello", ' ', 23, ' ', 3.14, " Bye! " );

    std::cout << s << '\n';
}
Khurshid
  • 2,654
  • 2
  • 21
  • 29
  • Very nice solution. I initially tried to use a stringstream but was having trouble doing it "in place", this makes it a lot more clear- thanks! – jett Feb 16 '14 at 16:25
  • 2
    What is the purpose of the `(/* do stuff with args*/, 0)` syntax? Since it's comma operator, it will take the right hand portion of the expression for each item in the variadic parameter? Essentially creating an array of zeros? Am I understanding this correctly? – void.pointer Sep 16 '14 at 02:32
  • 2
    @Khurshid Why it goes `{(0, ((stream << args), 0), 0) ...}`? What happens there? – psyched Feb 25 '15 at 13:49
  • @romanp: This call creates a (compile-time) recursive initialization list construction to create an unused, unnamed, dummy `int` array, but the expression `( (void)(stream << args), 0 )` has the side-effect of streaming the args into the stream (before returning the second `0`). – Adi Shavit Jun 08 '16 at 13:16
  • This is an amazing solution, does anyone have a resource that talks about clever stuff like this? – I NN_ Aug 08 '21 at 21:50
7

Three more ways to do this:

  1. Similar to Khurshid's but without unnecessary array of ints
  2. Similar to Simple's and Khurshid's but builds on older compilers
  3. Recurent way

And the code:

#include <iostream>
#include <sstream>

// First version:
template<typename T>
std::string toString(T value)
{
    std::ostringstream oss;
    oss << value;
    return oss.str();
}

std::string merge(std::initializer_list<std::string> strList)
{
    std::string ret = "";
    for (std::string s : strList) {
        ret += s;
    }
    return ret;
}

template< typename ... Args >
std::string stringer1(const Args& ... args)
{
    return merge({toString(args)...});
}


// Second version:
template< typename ... Args >
std::string stringer2(const Args& ... args)
{
    std::ostringstream oss;
    int a[] = {0, ((void)(oss << args), 0) ... };

    return oss.str();
}


// Third version:
template<typename T>
std::string stringer3(const T& value)
{
    std::ostringstream oss;
    oss << value;
    return oss.str();
}

template<typename T, typename ... Args >
std::string stringer3(const T& value, const Args& ... args)
{
    return stringer3(value) + stringer3(args...);
}

int main()
{
    int a, b;
    std::cout << stringer1("a", 1) << std::endl;
    std::cout << stringer2("b", 2) << std::endl;
    std::cout << stringer3("c", 3) << std::endl;

// Output:
//     a1
//     b2
//     c3
}
Janek Olszak
  • 4,067
  • 1
  • 28
  • 22
  • This seems less efficient - we create a new `stringstream` for every object we need to convert, then we use `string`'s `+=` operator which continuously reassigns memory. – c z Feb 20 '23 at 10:30
6

With C++17 we now have fold expressions to simplify this:

#include <sstream>

// cat - Concatenate values of arbitrary types into a string.
template <typename... Ts>
std::string cat(Ts&&... args) {
  std::ostringstream oss;
  (oss << ... << std::forward<Ts>(args));
  return oss.str();
}
Matt
  • 20,108
  • 1
  • 57
  • 70
3

Per request, here's a (longer) solution with SFINAE:

namespace detail {
    using std::to_string;

    std::string
    concat()
    {
        return "";
    }

    template<typename Head, typename... Tail>
    decltype( to_string(std::declval<Head>()) )
    concat(Head&& h, Tail&&... t);

    template<typename Head, typename... Tail>
    decltype(std::string() + std::declval<Head>())
    concat(Head&& h, Tail&&... t)
    {
        return std::forward<Head>(h) + concat(std::forward<Tail>(t)...);
    }

    template<typename Head, typename... Tail>
    decltype( to_string(std::declval<Head>()) )
    concat(Head&& h, Tail&&... t)
    {
        return to_string(std::forward<Head>(h)) + concat(std::forward<Tail>(t)...);
    }
}

template<typename... Args>
std::string concat(Args&&... args)
{
    return detail::concat(std::forward<Args>(args)...);
}

Can be seen in action here: http://coliru.stacked-crooked.com/a/77e27eaabc97b86b

Note that it assumes, for a given type, either string concatenation (with +) or to_string() is defined, but not both. So std::string, const char* and any 3rd party string class that interacts naturally with std::string should go through the + version. Of course, if the 3rd party string class does something silly like defining both concatenation and to_string() this will be ambiguous; you would need to define has_string_concat and has_to_string type predicates to have control on how to resolve the ambiguity.

I'm also putting everything in a namespace to be able to use argument-dependent lookup to select the right version of to_string; the full example shows a user-defined type with its own to_string().

DanielKO
  • 4,422
  • 19
  • 29
3

In C++ 11, doing it in variadic templates way works for me. It also enforces user to provide at least one argument. inline is helpful if you are defining functions in header file.

#include <sstream>

template< typename T >
inline std::string stringify(const T& t)
{
    std::stringstream string_stream;
    string_stream << t;
    return string_stream.str();
}

template< typename T, typename ... Args >
inline std::string stringify(const T& first, Args ... args)
{
    return stringify( first ) + stringify( args... );
}
shaffooo
  • 1,478
  • 23
  • 28
  • 1
    _"`inline` is helpful if you are defining functions in header file."_ No, it adds nothing, because template functions are implicitly inline anyway: https://stackoverflow.com/questions/11527415/are-methods-of-templated-classes-implied-inline-linkage – underscore_d Jun 25 '17 at 09:42