10

Consider an example:

#include <type_traits>

template <class... Ts>
decltype (auto) foo(Ts... ts) {
      return (ts->x + ...);
}

struct X {
    int x;
};

int main() {
    X x1{1};
    static_assert(std::is_reference_v<decltype(foo(&x1))>);
}

[live demo]

decltype(auto) deduced from parenthesized lvalue should according to [cl.type.simple]/4.4 be deduced to lvalue reference. E.g.:

decltype(auto) foo(X *x) { // type of result == int&
    return (x->x);
}

But the snipped triggers static_assert. Even if we compose expression into additional parentheses, e.g.:

return ((ts->x + ...));

It doesn't change the effect.

Is there a point in the standard that prevents deduction of a fold-expression of a single element into the lvalue reference?


Edit

As a great point of Johannes Schaub - litb clang actually does interpret the double-parens-version of the code as parenthesized lvalue and deduce lvalue reference. I'd interpret it as a gcc bug in this case. The version with single-parens-version is however still under the question. What puzzles me is that the version must be transformed into a parenthesized code at least in case of more than one element - to fulfil operator precedence. E.g.:

(x + ...)*4 -> (x1 + x2)*4

What is the reason of the inconsistency?

W.F.
  • 13,888
  • 2
  • 34
  • 81
  • Compiler? Or do Clang and GCC behave the same here? – StoryTeller - Unslander Monica Oct 31 '17 at 11:22
  • @StoryTeller yep - both – W.F. Oct 31 '17 at 11:22
  • Well, at least in regard to one set of parentheses, [temp.variadic/9](https://timsong-cpp.github.io/cppwp/n4659/temp.variadic#9) indicates that for one element, `(ts->x + ...)` is transformed into `x->x`, not `(x->x)`. As for the extra set of parentheses, good question. – StoryTeller - Unslander Monica Oct 31 '17 at 11:28
  • @StoryTeller VS also seems to triggers static_assert. Also in case of (ts->x + ...)*4 this should be transformed into (ts1->x + ts2->x)*4 in the case of two parameters, no? – W.F. Oct 31 '17 at 11:31
  • Ah... that's a good point, actually. The way a unary right and left fold are expanded seem to indicate drastic differences in the result, if there are no "surrounding" parentheses. So it seems the fold expression must be a single "thing". But its value category is somewhat fluid. – StoryTeller - Unslander Monica Oct 31 '17 at 11:37
  • 1
    @AndyG so in [this case](https://wandbox.org/permlink/0zMRhbdOLlXNoxBL) should it also be transformed to rvalue? – W.F. Oct 31 '17 at 11:47
  • @W.F: Yes because referring to a non-reference member by name has an expression of type T, not T&. You need another set of parentheses around `ts` – AndyG Oct 31 '17 at 12:00

2 Answers2

11

If you want a reference to be returned in the single parameter case, you'll need an extra set of parenthesis* so that the parameter pack expansion expands references:

template <class... Ts>
decltype (auto) foo(Ts... ts) {
      return ((ts->x) + ...);
}

Unfortunately in the scenario of multiple arguments being passed, the result is still the summation of integers, which returns an rvalue, so your static assertion will fail then.


Why doesn't ((ts->x + ...)) evaluate to a reference?

Because the fold expression will return an expiring int and then wrapping that int in another level of parentheses is still an int. When we use the inner parentheses ((ts->x) + ...)) the fold expression for a single argument returns int&. It does for clang 6.0.0 but not gcc 8.0.0, so I'm not sure.

*The fold expression's parentheses (the parentheses involved in (ts->x + ...)) are required as part of the fold expression; saying ts->x + ... alone is ill-formed.

W.F.
  • 13,888
  • 2
  • 34
  • 81
AndyG
  • 39,700
  • 8
  • 109
  • 143
  • Fantastic workaround (intended approach?). Shame it's kinda un-intuitive. – StoryTeller - Unslander Monica Oct 31 '17 at 12:02
  • Haven't took this into consideration. Sounds logical. Still can't see why the version without additional parentheses is not treated as lvalue... – W.F. Oct 31 '17 at 12:03
  • @W.F: I believe it has to do with [expr.ref] "Class member access". If I'm reading it correctly it's because accessing the member by name produces an expression that lacks a reference. – AndyG Oct 31 '17 at 12:30
  • @AndyG which point are you referring to? [\[expr.ref\]/4.2](http://eel.is/c++draft/expr.ref#4.2) would suggest otherwise: *If E1 is an lvalue, then E1.E2 is an lvalue* – W.F. Oct 31 '17 at 13:10
  • @W.F. My interpretation was that the distinction between "lvalue" and "lvalue reference" is important here. Because it only specifies the former, decltype() will return the type without the reference. The same as saying something like `int a = 1; static_assert(std::is_same_v);` – AndyG Oct 31 '17 at 13:30
  • @AndyG but the `decltype((a))` will produce reference just like `decltype(((a)))`. `a` is also lvalue not lvalue reference. My perception of `decltype((ts->x + ...))` transforms it in case of a single element into `decltype((ts1->x))` and `decltype(((ts->x + ...)))` into `decltype(((ts1->x)))`. So if `ts1->x` is lvalue `decltype((ts1->x))` should also be lvalue reference, no? – W.F. Oct 31 '17 at 13:38
  • 1
    @W.F. I think your understanding is close, but just off. The parentheses around the fold expression are there because a fold expression requires it. This is distinct from parentheses around a id expression, which transforms the expression type into a reference. So, to combine the two, we need two sets of parentheses in the fold expression; the fold expression's parentheses, and then separate parentheses around the id expression `ts->x` – AndyG Oct 31 '17 at 14:04
  • @AndyG ok wait I think I'm starting to follow your interpretation... the parenthesized expression is not id expression just fold expression which hand to be transformed firstly... – W.F. Oct 31 '17 at 16:31
  • This is good, but doesn't explain why the double-outer-parenthesis doesn't do the job aswell. – Johannes Schaub - litb Nov 01 '17 at 13:49
  • @JohannesSchaub-litb: Good point. I'm not totally sure, got any ideas? The standard doesn't really say anything about evaluating a fold expression of a single argument. I would imagine that a fold expression evaluates all of its arguments at the very least, so `(ts->x + ...)` is the same as simply evaluating `ts->x` which will return an expiring integer. Wrapping that up in parentheses leaves it an integer. – AndyG Nov 01 '17 at 14:30
  • @JohannesSchaub-litb: I think I agree more with my previous comment. Of course a fold expression should return the result of the fold, which means evaluating the arguments. Wrapping it up in an extra paren won't help if the result is "int". – AndyG Nov 01 '17 at 14:52
  • 1
    @AndyG of course the result is int. But the question is, whether it's an lvalue and whether there are parentheses around. All three of these influence the type yield by `decltype`. I think that clearly, it's an int. And clearly there are parens around. And clearly, it's an lvalue ("ts->x"). Evaluating "ts->x" yields an object. Definitely not anymore expiring than main's x1. – Johannes Schaub - litb Nov 01 '17 at 15:26
  • 3
    BTW, Clang accepts and doesn't fail the static assert: https://godbolt.org/g/FC8qvR – Johannes Schaub - litb Nov 01 '17 at 15:29
  • @JohannesSchaub-litb: Good find. Looks like they're implemented a bit differently in gcc and clang. My gut tells me that you and clang are right, if only because you're almost always right. On the other hand, perhaps the standard is merely not specific enough. – AndyG Nov 01 '17 at 15:35
  • 1
    This would suggest the bug in gcc (at least for the double-brace-version), no? – W.F. Nov 02 '17 at 18:36
  • 4
    I'm strongly advocating the bug is in GCC. `decltype(auto)` may not respond to the "parentheses" of the fold expression, but the extra set is something it should explicitly be affected by. – StoryTeller - Unslander Monica Nov 06 '17 at 08:56
0

After a small investigation it turns out that standard does not provide any explicit guarantees on the parenthesizeness of an expression produced from fold expression [temp.variadic]/9 (emphasis mine):

The instantiation of a fold-expression produces:

  • ((E1 op E2) op ⋯) op EN for a unary left fold,
  • E1 op (⋯ op (EN−1 op EN)) for a unary right fold,
  • (((E op E1) op E2) op ⋯) op EN for a binary left fold, and
  • E1 op (⋯ op (EN−1 op (EN op E))) for a binary right fold.

In each case, op is the fold-operator, N is the number of elements in the pack expansion parameters, and each Ei is generated by instantiating the pattern and replacing each pack expansion parameter with its ith element. For a binary fold-expression, E is generated by instantiating the cast-expression that did not contain an unexpanded parameter pack.

Example:

template<typename ...Args>
  bool all(Args ...args) { return (... && args); }

bool b = all(true, true, true, false);

Within the instantiation of all, the returned expression expands to ((true && true) && true) && false, which evaluates to false. — end example

If N is zero for a unary fold-expression, the value of the expression is shown in Table 14; if the operator is not listed in Table 14, the instantiation is ill-formed.

This would suggest that there is no guarantees about the parenthesize even when of the fold expression operator would affect the change of the operators precedence of the produced expression which seems to be a standard defect to me. E.g. (ts + ...)*4 should be expanded to ts1 + ts2*4 and not (ts1 + ts2)*4 when literally apply the rule in the example from standard.

Nevertheless as the single element parameter pack isn't considered in the fold expression production rule it is underspecified what should parameter pack produce in this case and hence both parenthesized version as well as unparenthesized one conform the standard.

W.F.
  • 13,888
  • 2
  • 34
  • 81