Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

C++17 Variadic Template Folding

I don't understand why this doesn't work. Could someone who understands templates and variadic expression folding explain what is going on and give a solution that does work?

#include <iostream>
#include <string>

template <typename... Args>
void print(Args... args)
{
    std::string sep = " ";
    std::string end = "\n";
    (std::cout << ... << sep << args) << end;
}

int main()
{
    print(1, 2, 3);
}

It should print out each of the args with a space in between and a newline at the end. It works if you remove the sep << but then there is no space between each argument when it is printed.

like image 648
nickeb96 Avatar asked May 01 '17 13:05

nickeb96


3 Answers

The grammar for binary fold-expressions must be one of:

(pack op ... op init)
(init op ... op pack)

What you have is (std::cout << ... << sep << args), which doesn't fit either form. You need something like (cout << ... << pack), which is why removing sep works.

Instead, you can either fold over a comma:

((std::cout << sep << args), ...);

or use recursion:

template <class A, class... Args>
void print(A arg, Args... args) {
    std::cout << arg;
    if constexpr (sizeof...(Args) > 0) {
        std::cout << sep;
        print(args...);
    }
}
like image 88
Barry Avatar answered Oct 17 '22 05:10

Barry


This will work, but it will print a trailing space:

template <typename... Args>
void print(Args... args)
{
    std::string sep = " ";
    std::string end = "\n";

    ((std::cout << args << sep), ...) << end;
}

live wandbox example


In this case, a fold over the comma operator is being performed, resulting in an expansion like:

// (pseudocode)
(std::cout << args<0> << sep), 
(std::cout << args<1> << sep),
(std::cout << args<2> << sep), 
...,
(std::cout << args<N> << sep), 
like image 15
Vittorio Romeo Avatar answered Oct 17 '22 04:10

Vittorio Romeo


What you really want to do is:

std::string sep = " ";
std::string end = "\n";
(std::cout << ... << (sep << args)) << end;

because you want (sep << args) to be left-folded with std::cout. This doesn't work, because sep << args doesn't know it is being streamed to std::cout or streamed at all; << is only streaming if the left hand side is a stream.

In short, the problem is that sep << args doesn't understand it is streaming.

Your other problem is not enough lambda.

We can fix this.

template<class F>
struct ostreamer_t {
    F f;
    friend std::ostream& operator<<(std::ostream& os, ostreamer_t&& self ) {
        self.f(os);
        return os;
    }
    template<class T>
    friend auto operator<<(ostreamer_t self, T&& t) {
        auto f = [g = std::move(self.f), &t](auto&& os)mutable {
            std::move(g)(os);
            os << t;
        };
        return ostreamer_t<decltype(f)>{std::move(f)};
    }
};

struct do_nothing_t {
    template<class...Args>
    void operator()(Args&&...)const {}
};

const ostreamer_t<do_nothing_t> ostreamer{{}};

template <typename... Args>
void print(Args... args)
{
    std::string sep = " ";
    std::string end = "\n";
    (std::cout << ... << (ostreamer << sep << args)) << end;
}

live example. (I also used a literal for sep to ensure I work with rvalues).

ostreamer captures references to things it is <<'d, then dumps them when in turn it is << to an ostream.

This entire process should be transparent to the compiler, so a decent optimizer should evaporate everything involved.

like image 11
Yakk - Adam Nevraumont Avatar answered Oct 17 '22 04:10

Yakk - Adam Nevraumont