Search code examples
c++c++17variadic-templatesoperator-keywordfold

Fold expression with comma operator and variadic template parameter pack


#include<iostream>
using namespace std;

template<typename ...Args>
void output_argus(Args&&... args) 
{
    ((cout << args << '\n'), ...);    // #1
    (... , (cout << args << '\n'));   // #2
}


int main()
{
    output_argus(1, "test", 5.6f);
}

Based on c++ operator doc, ',' is a left to right operator. It is meaning a, b, c, d meaning (((a, b), c),d) not (a, (b, (c, d))). This is important if a, b, c, d are statements.

However, based on fold expression doc, for ',' which should use unary left fold.

My question why both statements in my code are working? Shouldn't only #2 work? And also how to understand ... and args. and nested fold expression?


Solution

  • Let's say we're folding 3 expressions over a binary operator, with a unary fold. We have two options here: (xs @ ...) (a unary right fold) and (... @ xs) (a unary left fold).

    (xs @ ...) expands out to (a @ (b @ c))

    (... @ xs) expands out to ((a @ b) @ c)

    What can we say about the difference between the expressions a @ (b @ c) and (a @ b) @ c? If @ is associative over these types, then those two expressions are identical. That's what associative means. If you had a parameter pack of integers, then a unary left fold over + and a unary right fold over + will have the same value (modulo overflow), because addition is associative. Subtraction, on the other hand, is not associative. (xs - ...) and (... - xs) mean very different things.

    Likewise, the , operator in C++ is associative. It doesn't matter which way you parenthesize the expressions. ((a, b), c) and (a, (b, c)) both evaluate and discard a, then evaluate and discard b, then evaluate c and that's the result. It's easier to see if you reduce the expressions to just letters why this is the case.

    As a result, both ((cout << args << '\n'), ...) and (... , (cout << args << '\n')) do the same thing, and they both effectively mean:

    cout << args1 << '\n';
    cout << args2 << '\n';
    // ...
    cout << argsN << '\n';