Reputation: 1777
I am trying to write a function that can convert its argument into a string. However, I am finding it difficult to unpack the parameter pack.
Here is the code that I have written:
#include <iostream>
#include <sstream>
template <typename... T>
std::string StringFormatter(T... values)
{
std::ostringstream out;
for (auto&& x : { values... }) {
out << x;
}
return out.str();
}
int main()
{
auto&& i = StringFormatter("One ", "two"); //Success
auto&& j = StringFormatter("one ", 1, "two", 2.0); //Fails
std::cout << i;
}
I know that the above code is failing because the initializer list accepts only single type arguments.
I have tried a recursive approach to achieve the above implementation, but no luck.
If you can suggest a better way to achieve this, it would be a great help.
Upvotes: 0
Views: 567
Reputation: 6086
In short:
If you don't have a C++17 compiler, you can rely on the int array trick:
template <typename... T>
std::string StringFormatter(T... values) {
std::ostringstream out;
int arr[] = { 0, (out << values, void(), 0)... };
return out.str();
}
The apparently useless 0
at the start of the array is required in the case the parameter pack is empty because you can't instantiate an array of size 0. The void()
is there to circumvent hypothetical operator,
overloads.
The evaluation order is guaranteed and the compiler should be able to optimize away the array in the resulting binary.
In depth:
This technique is the pre-C++17 way of doing fold expressions. Basically we create an array of sizeof...(T) + 1
elements (all 0). The catch here is that we are using properties of the ,
operator to run the operation we want on each element of the parameter pack.
Let's forget about the parameter pack and the template for a moment. When you do:
something, other_thing
Assuming there is no overload to the ,
operator, the statement is evaluated to other_thing
. But that doesn't mean that something
is ignored. Its value is just discarded in favor of other_thing
. We are using that property for our little trick.
int x = 0;
int a[] = { 0, (++x, 0) }; // a is {0, 0}, x is 1
Now since you can overload operator,
, we just add an additional statement to avoid this hypothetical overload:
(something, void(), 0)
Since operator,
is a binary operator, an overloaded version of it cannot have only one argument. By adding a statement evaluating to void
we are preventing any hypothetical overload to be picked and therefore are sure we end up with our 0
.
The last step is to combine that with our parameter pack and perform pack expansion on the resulting statement:
(out << values, void(), 0)...
Upvotes: 3
Reputation: 490663
There are better ways to do it now (with a fold expression), but if you want to use the recursive approach, it can look something like this:
#include <sstream>
#include <string>
#include <iostream>
template <class T>
std::string stringify(T const &t) {
std::stringstream b;
b << t;
return b.str();
}
template<typename T, typename... Args>
std::string stringify(T arg, const Args&... args) {
return stringify(arg) + stringify(args...);
}
int main() {
std::string three{" three"};
std::cout << stringify("one: ", 1, " two: ", 2, three, "\n");
return 0;
}
You should be able to use this with essentially any type that supports stream insertion. If you're passing enough parameters that the quadratic time on the number of parameters is a concern, 1) go see a psychiatrist, and 2) feel free to use code more on this general order:
#include <sstream>
#include <string>
#include <iostream>
namespace detail {
template <class T>
void stringify(std::ostringstream &b, T const &t) {
b << t;
}
template<typename T, typename... Args>
void stringify(std::ostringstream &os, T arg, const Args&... args) {
stringify(os, arg);
stringify(os, args...);
}
}
template <typename ...Args>
std::string stringify(const Args &...args) {
std::ostringstream os;
detail::stringify(os, args...);
return os.str();
}
int main() {
std::string three{" three"};
std::cout << stringify("one: ", 1, " two: ", 2, three, "\n");
}
...but definitely see a psychiatrist first. If you're passing enough arguments for it to matter, you're clearly doing something horribly wrong.
Upvotes: 3
Reputation: 30010
You can achieve this with C++17's fold expression:
template <typename... T>
std::string StringFormatter(T... values)
{
std::ostringstream out;
(out << ... << values);
return out.str();
}
Upvotes: 4