Clang and the binary fold expressions — The curse

2019-04-04 14:23发布

问题:

Specifically Clang 3.6.0, the one currently hosted by Coliru.

All these snippets are called from :

int main() {
    foo();
    std::cout << "\n----\n";
    foo(1, 2, 3);
}

The following code :

template <class... Args>
void foo(Args... args) {
    std::cout << ... << args;
}

Triggers the following compilation error :

main.cpp:7:17: error: expected ';' after expression
    std::cout << ... << args;
                ^
                ;
main.cpp:7:15: error: expected expression
    std::cout << ... << args;
              ^

So I tried putting parentheses around the expression :

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

It works, but triggers a warning :

main.cpp:7:6: warning: expression result unused [-Wunused-value]
    (std::cout << ... << args);
     ^~~~~~~~~
main.cpp:11:5: note: in instantiation of function template specialization 'foo<>' requested here
    foo();
    ^

So I tried to discard the value of the expression with a function-style cast to void :

void(std::cout << ... << args);

But :

main.cpp:7:20: error: expected ')'
    void(std::cout << ... << args);
                   ^
main.cpp:7:9: note: to match this '('
    void(std::cout << ... << args);
        ^

I tried a static_cast too, for the same result.

So I tried with a C-cast instead :

(void)(std::cout << ... << args);

But then :

main.cpp:6:18: warning: unused parameter 'args' [-Wunused-parameter]
void foo(Args... args) {
                 ^

... and my output is only ---- : foo(1, 2, 3); doesn't output anymore !

Is Clang cursed by an evil force from future standards, does it have a bug, or is the problem sitting on my chair right now ?

回答1:

You need an extra set of parentheses when casting to void using the functional notation cast, otherwise the parentheses are considered part of the cast expression instead of the fold expression. The fold expression syntax itself requires a set of parentheses.

All of the following work without producing any warnings:

void((std::cout << ... << args));
(void)((std::cout << ... << args));

Or just call some ostream member function to avoid the unused result warning

(std::cout << ... << args).flush();

As T.C. mentions in the comments below, the behavior with (void)(std::cout << ... << args); seems like a clang bug. The syntax for a cast notation is specified in 5.4 [expr.cast]

cast-expression:
  unary-expression
  ( type-id ) cast-expression

Since parentheses are not required as part of the cast expression, that usage shouldn't be producing warnings, and more importantly, it should result in printing the arguments.



回答2:

I decided to take a better look at this bug in the Clang source. Here's the offending section of code. This case happens when it's just finished parsing (<type>) and is now parsing a following parenthesized expression:

} else if (isTypeCast) {
  // Parse the expression-list.
  InMessageExpressionRAIIObject InMessage(*this, false);

  ExprVector ArgExprs;
  CommaLocsTy CommaLocs;

  if (!ParseSimpleExpressionList(ArgExprs, CommaLocs)) {
    // FIXME: If we ever support comma expressions as operands to
    // fold-expressions, we'll need to allow multiple ArgExprs here.
    if (ArgExprs.size() == 1 && isFoldOperator(Tok.getKind()) &&
        NextToken().is(tok::ellipsis))
    return ParseFoldExpression(Result, T);

    ExprType = SimpleExpr;
    Result = Actions.ActOnParenListExpr(OpenLoc, Tok.getLocation(),
                                        ArgExprs);
  }
}

// The beginning of ParseFoldExpression(LHS, T):
if (LHS.isInvalid()) {
  T.skipToEnd();
  return true;
}

The specific part of the code responsible for this bug is here:

return ParseFoldExpression(Result, T);

It turns out that Result is never set apart from its initial true value. I believe it should be set to ArgExprs.front(), which now holds std::cout.

Now you'll notice the FIXME as well. While unrelated to this issue in particular, it might be worth fixing alongside this.

Being my first Clang fix, I still have several things to do before submitting a change (for reference, Clang 4.0 is currently in development). I would be more than happy for this to be fixed at all, whether it's by me or someone else. At the very least, my findings are documented somewhere for now.



回答3:

A fold expression, from [expr.prim.fold] is:

A fold expression performs a fold of a template parameter pack (14.5.3) over a binary operator.
    fold-expression:
        ( cast-expression fold-operator ... )
        ( ... fold-operator cast-expression )
        ( cast-expression fold-operator ... fold-operator cast-expression )

Note that in all cases, the parentheses are part of the grammar. So your initial example is syntactically incorrect, and must be:

template <class... Args>
void foo(Args... args) {
    (std::cout << ... << args);
}

That will then give you a warning in the case of an empty pack, since the binary fold reduces to just std::cout; To get rid of that warning, you can go the usual route of casting to void - just that the inner set of parentheses are part of the grammar so you need two:

void((std::cout << ... << args));

Or you could just throw in an extra endl or the like:

(std::cout << ... << args) << std::endl;

Or return the result:

template <class... Args>
std::ostream& foo(Args... args) {
    return (std::cout << ... << args);
}