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.

\$\endgroup\$
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).

\$\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();
\$\endgroup\$

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.