Is the value category of a fold expression always prvalue?

Viewed 224

Is a fold expression always a prvalue? Is this correct?

template<typename... Args>
auto sum(Args... args) {
    auto fold = (... +  args);
    return fold;
}

int main() {
    sum(10, 2, 2);
}

I‘m really only interested in the fold expression which is (... + args) in the above example.

4 Answers

A fold-expression has the same semantics as simply writing out the N - 1 applications of the operator (where N is the number of elements in the pack). For example, sum(10, 2, 2) will produce (10 + 2) + 2. See [temp.variadic]/9.

In general, this may or may not be an prvalue. Folding 2 or more numeric values with + will always yield a prvalue since the built-in + operator yields a prvalue, but if there is only one element in the pack args, then (... + args) means the same thing as simply mentioning that one element by its hypothetical name, so the result would be an lvalue. And of course you can fold with other (possibly overloaded) operators too, which may produce glvalues.

Doesn't have to be. It all depends on the operator. If you were to use += instead (a somewhat contrived use), the resulting expression would not be a prvalue, but an lvalue.

To clarify:

template<typename... Args>
auto sum(Args... args) {
    auto fold = (... +=  args) = 3;
    return fold;
}

Since the result of += is an lvalue designating the left hand operand (I'm discounting crazy overloads for simplicity), the fold expression also ends up producing an lvalue. So the assignment at the end is perfectly valid, though smelly.

Fold expressions don't effect what the expression's value category is, it is literally only just the compiler expanding the parameter pack for you

template<typename... Args>
void foo(Args&... args)
{
    (args, ...) = 42;
}

void bar()
{
    int x, y, z;
    foo(x, y, z);
}

Is valid, since (args, ...) has type int&

I really like the answers by @Brian and @Passer By; it has the same semantics as just expanding the parameter pack with the operator. I'd like to add a (contrived) example to demonstrate what that means.

Let's modify your sum function first to use decltype(auto) and forwarding semantics:

template<typename... Args>
decltype(auto) sum(Args&&... args) {
    decltype(auto) fold = (... +  std::forward<Args>(args));
    return fold;
}

Still functionally the same, only now sum will return exactly the type that fold is, and fold will take on the exact type of the summation.

Our fold expression is called a unary left fold and its expansion will look like this:

((E1 op E2) op ...) op EN

More concretely, assume that Args can be treated like an array of length N:

((Args[0] + Args[1]) + ...) + Args[N-1]

Which is basically the same as

Args[0] + Args[1] + ... + Args[N-1]

So now that we've taken some mysticism out of the whole thing it doesn't take much to see that the type of the expression is really just the type of the result of adding up whatever types come across on both sides of the + operator.

I can contrive the following type Foo that has some strange addition semantics:

const std::string global = "Global string";

struct Foo{
    Foo(int){}
};

const std::string& operator +(const Foo&, const Foo&){
    return global;
}
const std::string& operator +(const std::string& _str, const Foo&){
    return _str;
}

And now when I call sum for instances of Foo, I'll receive a const std::string& in return, which is definitely not a prvalue:

int main() {
    std::cout << sum(Foo{10}, Foo{2}, Foo{2}) << std::endl;
}

Prints

Global string

Related