What is the easiest way to print a variadic parameter pack using std::ostream?

23,713

Solution 1

Without recursive calls and commas where you wanted.

In / through parameter pack expansion:

template <typename Arg, typename... Args>
void doPrint(std::ostream& out, Arg&& arg, Args&&... args)
{
    out << std::forward<Arg>(arg);
    using expander = int[];
    (void)expander{0, (void(out << ',' << std::forward<Args>(args)), 0)...};
}

DEMO


In using fold expressions:

template <typename Arg, typename... Args>
void doPrint(std::ostream& out, Arg&& arg, Args&&... args)
{
    out << std::forward<Arg>(arg);
    ((out << ',' << std::forward<Args>(args)), ...);
}

DEMO 2

Solution 2

In C++17, there will be an easier way (as hinted at by Kerrek SB in comments; this was actually present in N4606, the first post-C++14 draft), called fold expressions:

The code would be:

(out << ... << args);

and the pattern expression op ... op parameter-pack is called a binary left fold, whose definition is equivalent to ((( expression op arg1) op arg2) op arg3) .... op argN.

I think the outer parentheses are not strictly necessary for an expression-statement like this, but if the fold expression is an operand of another operator then they are either required, or a very good idea :)

Solution 3

The usual answer is to define two separate overloads, with an empty one for the base case:

// base case
void doPrint(std::ostream& out) {}

template <typename T, typename... Args>
void doPrint(std::ostream& out, T t, Args... args)
{
    out << t;                // add comma here, see below
    doPrint(out, args...);
}

Of course in real code I wouldn't make copies of the arguments each time and instead use forwarding references, but you get the idea.

If you want to add commas after every item, even after the last one, just replace out << t with out << t << ','.

If you only want commas on the inside, not past the last element, you need a separate one-argument overload which doesn't print the comma, and a generic overload take two distinct arguments before the pack, i.e:

template <typename T>
void doPrint(std::ostream& out, T t)
{
    out << t;
}

template <typename T, typename U, typename... Args>
void doPrint(std::ostream& out, T t, U u, Args... args)
{
    out << t << ',';
    doPrint(out, u, args...);
}

Solution 4

Parameter pack expansion only works in plain function calls, not for infix operators. Hence, you need to convert s << x syntax into plain function call syntax f(s, x):

template<class Head>
void print_args_(std::ostream& s, Head&& head) {
    s << std::forward<Head>(head);
}

template<class Head, class... Tail>
void print_args_(std::ostream& s, Head&& head, Tail&&... tail) {
    s << std::forward<Head>(head);
    print_args_(s, std::forward<Tail>(tail)...);
}

template<class... Args>
void print_args(Args&&... args) {
    print_args_(std::cout, std::forward<Args>(args)...);
}

Solution 5

The generic form that works with std::wostream as well:

template <typename CharT, typename Traits>
std::basic_ostream<CharT, Traits> &
Print(std::basic_ostream<CharT, Traits> &out)
{
    return out;
}

template <typename CharT, typename Traits, typename T>
std::basic_ostream<CharT, Traits> &
Print(std::basic_ostream<CharT, Traits> &out, T &&t)
{
    return (out << std::forward<T>(t));
}

template <typename CharT, typename Traits, typename T, typename... Args>
std::basic_ostream<CharT, Traits> &
Print(std::basic_ostream<CharT, Traits> &out, T &&t, Args &&...args)
{
    return Print( Print(out, std::forward<T>(t)), std::forward<Args>(args)... );
}

I couldn't make it work with with std::endl in the common case (it is possible to handle std::endl in specific cases like when it is the first or the last argument, but not in the common case, especially if there are multiple std::endl in a single call). You can still use '\n' instead or use std::endl with template arguments specified if you really need std::endl:

Print(std::cout, "hello world", std::endl<char, std::char_traits<char>>);

The differences between std::endl and '\n'

  • If the stream is working in binary mode then '\n' isn't converted to the line ending format of the platform the code compiled for (but in text mode it is still converted).
  • '\n' doesn't flush the stream with std::flush (but it still flushes the std::cout if the program is running on a terminal)

So for me it is OK to use '\n', or maybe even preferred.

Using some other IO manipulators is still possible:

Print(std::cout, std::hex, 11, '\n');

I also implemented sprintf counterpart that works with variadic templates and returns std::string:

template <typename CharT = char, typename Traits = std::char_traits<CharT>, typename... Args>
std::basic_string<CharT, Traits>
SPrint(Args &&...args)
{
    std::basic_stringstream<CharT, Traits> ss;
    Print(ss, std::forward<Args>(args)...);
    return std::move(ss.str());
}

Here is some demos.

Share:
23,713
gexicide
Author by

gexicide

I'm a long term developer of Tableau's Hyper database engine and currently the manager in charge of the Hyper API.

Updated on May 30, 2020

Comments

  • gexicide
    gexicide almost 4 years

    What is the easiest way to print a parameter pack, separated by commas, using std::ostream?

    Example:

    template<typename... Args>
    void doPrint(std::ostream& out, Args... args){
       out << args...; // WRONG! What to write here?
    }
    
    // Usage:
    int main(){
       doPrint(std::cout,34,"bla",15); // Should print: 34,bla,15
    }
    

    Note: It may be assumed that a corresponding overload of the << operator is available for all types of the parameter pack.

  • gexicide
    gexicide over 9 years
    Can you elaborate a bit on the forwarding references? I am not too firm in that C++11 stuff, yet, and copying the parameters around really seems to be an overkill.
  • Kerrek SB
    Kerrek SB over 9 years
    @gexicide: thousands of duplicates on this site, just search around. You're looking for Args &&... args and std::forward<Args>(args)....
  • Germán Diago
    Germán Diago over 9 years
    This one is the easiest. It is just that it looks weird, hehe.
  • Maxim Egorushkin
    Maxim Egorushkin over 9 years
    @GermánDiago en.cppreference.com/w/cpp/language/parameter_pack gives a similar example: int dummy[sizeof...(Ts)] = { (std::cout << args, 0)... };
  • Alexander Revo
    Alexander Revo almost 8 years
    As a side note, you can skip the using declaration and just write (void)(int[]){(std::cout << ',' << std::forward<Args>(args)), 0)...};. You also don't need the first zero in the braces or the inner void cast.
  • Alexander Revo
    Alexander Revo almost 8 years
    Btw, my understanding of forwarding is not exactly rock-solid, but there is no reason at all to use std::forward to insert a value into ostream, or is there?
  • Piotr Skotnicki
    Piotr Skotnicki almost 8 years
    @AlexanderRevo inserting into ostream means passing the argument to some overload of operator<<. you never know what parameter type the operator expects
  • Ian Gilham
    Ian Gilham over 7 years
    My C++ must be getting out-dated. I can't parse that at all.
  • Piotr Skotnicki
    Piotr Skotnicki over 7 years
    @IanGilham appologise if my answer is not clear. Please find an extensive explanation here
  • Max Matti
    Max Matti over 5 years
    To use separators: ((out << ", " << args), ...);
  • Moia
    Moia over 5 years
    I would change the constructor in = delete. Since there are only static method, it's pointless instantiate a Console object
  • DevSolar
    DevSolar over 5 years
    Is there some way to make this work for the one-parameter case as well (i.e., doPrint( std::cout, 1 ))? (Obviously we don't want the fold expression at all in that case, but how to magick that into being?)
  • DevSolar
    DevSolar over 5 years
    @PiotrSkotnicki: Ah. I was seeing an error, but that was because I appended a << std::endl to the fold expression. D'uh. ;-)
  • Larytet
    Larytet over 3 years
    @PiotrSkotnicki impressive, why do I need the left most cast to void in the C++11 version?