7
\$\begingroup\$

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:

  1. 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?

  2. Is my use of std::forward appropriate?

  3. Any other comments and suggestion are very welcome.

Jamal
35.2k13 gold badges134 silver badges238 bronze badges
asked Sep 10, 2014 at 2:52
\$\endgroup\$

2 Answers 2

5
\$\begingroup\$

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).

answered Sep 10, 2014 at 5:00
\$\endgroup\$
3
\$\begingroup\$

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();
answered Sep 10, 2014 at 16:01
\$\endgroup\$

Your Answer

Draft saved
Draft discarded

Sign up or log in

Sign up using Google
Sign up using Email and Password

Post as a guest

Required, but never shown

Post as a guest

Required, but never shown

By clicking "Post Your Answer", you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.