Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Assignment to a prvalue in C++

Tags:

c++

I was exploring a cppreference page about value categories and found some strange syntax in an example (the second extended content):

#include <iostream>
 
struct S
{
    S() : m{42} {}
    S(int a) : m{a} {}
    int m;
};
 
int main()
{
    S s;
 
    // Expression `S{}` is prvalue
    // May appear on the right-hand side of an assignment expression
    s = S{};
 
    std::cout << s.m << '\n';
 
    // Expression `S{}` is prvalue
    // Can be used on the left-hand side too
    std::cout << (S{} = S{7}).m << '\n';
}

Look at the last line of main(). There is an assignment S{} = S{7} that is very awkward. We can do this even simpler:

#include <iostream>

struct S {
    int m = 1;
};

int main()
{
    std::cout << (S{} = S{}).m << '\n';
}

Compiler shows no warning or errors and everything is completely fine. That could be okay, but I think the documentation says it must be impossible. Quoting the same page:

An rvalue can't be used as the left-hand operand of the built-in assignment or compound assignment operators.

I guess S{} is a prvalue, and all other conditions are met also. So, how does C++ converts this prvalue into an lvalue?

like image 979
Maxim Fomin Avatar asked May 08 '26 12:05

Maxim Fomin


2 Answers

The quote says "built-in assignment".

You are not using a built-in assignment. The built-in assignment operator operates on non-class types. You are using the assignment operator overload S& operator=(S&&); implicitly-declared as non-static member function of S. In other words, after overload resolution S{} = S{} is equivalent to

S{}.operator=(S{})

Calling a non-static member function (without reference-qualifiers) on a prvalue is permitted. It behaves just like binding the S{} parameter in the argument to the reference parameter: A temporary object of type S is created and initialized from the prvalue and this in the operator= overload refers to this temporary object, which lives until the end of the full-expression in which it was created.

Of course, that assignment is usually pointless, because the temporary object is destroyed at the end of the expression and so it doesn't matter what you assign to it. But sometimes it is useful to be able to chain member function calls on functions that return by-value, i.e. prvalues, if at the end only some result of the last member function call is needed. That's why it is allowed and permits this use without having to name each intermediate class object.

like image 98
user17732522 Avatar answered May 10 '26 01:05

user17732522


For historical reasons, the assignment operator the compiler generates by default is S &operator=(S &&); and NOT S &operator=(S &&) &; as one might expect. That means that you can call it on a prvalue without any error.

You can "fix" this by adding

S &operator=(S &&) & = default;

to your class definition. You probably also want

S &operator=(const S&) & = default;

those two should be the default (IMO) but for historical reasons (going back to before the introduction of rvalue types and move semantics) they are not.

like image 27
Chris Dodd Avatar answered May 10 '26 00:05

Chris Dodd



Donate For Us

If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!