I wanted to get better acquainted with variadic templates, so I decide to try to implement a function like D's writeln()
, just for fun.
void writeln()
{
std::cout << "\n";
}
template<typename T, typename ...Args>
void writeln(T firstArg, Args... extraArgs)
{
std::cout << firstArg;
writeln(std::forward<Args>(extraArgs)...);
}
Usage example:
writeln("hello ", "world ", 42, "\t", 3.141592);
writeln(); // prints just a newline
Next, I implemented a format()
function, which writes to a string instead of cout
:
// Need this because there is no to_string for string in the std namespace.
std::string to_string(const std::string & s)
{
return s;
}
std::string format()
{
return "";
}
template<typename T, typename ...Args>
std::string format(T firstArg, Args... extraArgs)
{
using namespace std;
std::string s = to_string(firstArg);
s += format(std::forward<Args>(extraArgs)...);
return s;
}
// sample:
std::string s = format("hello ", "world ", 42, "\t", 3.141592);
It is uses std::to_string()
for the native types. If I want to print custom types, then I can define a local to_string()
and the overload resolution should find it.
My concerns are:
I haven't had many chances to use variadic templates so far, so I might be missing some caveat here. I was expecting it to be more complicated... Did I miss some corner case?
Is my use of
std::forward
appropriate?Any other comments and suggestion are very welcome.
2 Answers 2
The biggest thing that sticks out at me is that you're passing all of your Args...
parameters by value, thus pretty much making std::forward
useless here. To make use of std::forward
, the reference type needs to be deduced from the calling context. By itself, std::forward
really doesn't do anything except a static_cast
to the deduced reference type.
Basically, everywhere you have Args
should be passed by Args&&
:
template<typename T, typename ...Args>
void writeln(T firstArg, Args&&... extraArgs)
template<typename T, typename ...Args>
std::string format(T firstArg, Args&&... extraArgs)
As a cut down example of the difference this makes, try this example:
#include <memory>
#include <iostream>
struct s
{
s()
{ }
s(s&& )
{
std::cout << "Move\n";
}
s(const s& )
{
std::cout << "Copy\n";
}
};
template <typename T>
void do_forward(T&& v)
{
sink(std::forward<T>(v));
}
template <typename U>
void sink(U&& u)
{
std::cout << "In sink\n";
}
int main()
{
s something;
do_forward(something);
}
If I run this, the output is:
In sink
If I change the signature of do_foward
and sink to:
template <typename T>
void do_forward(T v)
template <typename U>
void sink(U u)
The output is:
Copy
Move
In sink
If we change main
to just pass an actual rvalue
reference instead of an lvalue
:
int main()
{
do_forward(s{});
}
We remove the first copy operation (from do_forward
) but still have a move operation (in sink
).
Just a minor point as an addendum to Yuushi's answer, but instead of having separate functions for std::string
and std::cout
, you could instead modify it to use a generic stream class, for instance:
std::ostream& writeln( std::ostream& outStream )
{
outStream << std::endl;
return outStream;
}
template <typename T, typename... Args>
std::ostream& writeln( std::ostream& outStream, T tFirstArg, Args&&... args )
{
outStream << tFirstArg;
return writeln( outStream, std::forward<Args>(args)... );
}
You can then use this to write a line to a file by passing a std::ofstream
to writeln
, to the output stream by passing std::cout
, or to a string by using std::ostringstream
as
// Print output to screen:
writeln( std::cout, "hello ", "world ", 42, "\t", 3.141592f );
// Print output to a file, "testfile.txt":
std::ofstream outFile( "testfile.txt" );
if( outFile.good() )
{
writeln( outFile, "hello ", "world ", 42, "\t", 3.141592f );
}
// Get a string using writeln function:
std::ostringstream ssOutStringStream;
writeln( ssOutStringStream, "hello ", "world ", 42, "\t", 3.141592f );
std::string strOutput = ssOutStringStream.str();
Explore related questions
See similar questions with these tags.