(note the original question title had "instead of an rvalue" rather than "instead of a const reference". One of the answers below is in response to the old title. This was fixed for clarity)
One common construct in C and C++ is for chained assignments, e.g.
int j, k;
j = k = 1;
The second =
is performed first, with the expression k=1
having the side effect that k
is set to 1, while the value of the expression itself is 1.
However, one construct that is legal in C++ (but not in C) is the following, which is valid for all base types:
int j, k=2;
(j=k) = 1;
Here, the expression j=k
has the side effect of setting j
to 2, and the expression itself becomes a reference to j
, which then sets j
to 1. As I understand, this is because the expression j=k
returns a non-const
int&
, e.g. generally speaking an lvalue.
This convention is usually also recommended for user-defined types, as explained in "Item 10: Have assignment operators return a (non-const) reference to *this" in Meyers Effective C++(parenthetical addition mine). That section of the book does not attempt to explain why the reference is a non-const
one or even note the non-const
ness in passing.
Of course, this certainly adds functionality, but the statement (j=k) = 1;
seems awkward to say the least.
If the convention were to instead have builtin assignment return const references, then custom classes would also use this convention, and the original chained construction allowed in C would still work, without any extraneous copies or moves. For example, the following runs correctly:
#include <iostream>
using std::cout;
struct X{
int k;
X(int k): k(k){}
const X& operator=(const X& x){
// the first const goes against convention
k = x.k;
return *this;
}
};
int main(){
X x(1), y(2), z(3);
x = y = z;
cout << x.k << '\n'; // prints 3
}
with the advantage being that all 3 (C builtins, C++ builtins, and C++ custom types) all are consistent in not allowing idioms like (j=k) = 1
.
Was the addition of this idiom between C and C++ intentional? And if so, what type of situation would justify its use? In other words, what non-spurious benefit does does this expansion in functionality ever provide?
By design, one fundamental difference between C and C++ is that C is an lvalue-discarding language and C++ is an lvalue-preserving language.
Before C++98, Bjarne had added references to the language in order to make operator overloading possible. And references, in order to be useful, require that the lvalueness of expressions be preserved rather than discarded.
This idea of preserving the lvalueness wasn't really formalized though until C++98. In the discussions preceding the C++98 standard the fact that references required that the lvalueness of an expression be preserved was noted and formalized and that's when C++ made one major and purposeful break from C and became an lvalue preserving language.
C++ strives to preserve the "lvalueness" of any expression result as long as it is possible. It applies to all built-in operators, and it applies to built-in assignment operator as well. Of course, it is not done to enable writing expressions like (a = b) = c
, since their behavior would be undefined (at least under the original C++ standard). But because of this property of C++ you can write code like
int a, b = 42;
int *p = &(a = b);
How useful it is is a different question, but again, this is just one consequence of lvalue-preserving design of C++ expressions.
As for why it is not a const
lvalue... Frankly, I don't see why it should be. As any other lvalue-preserving built-in operator in C++ it just preserves whatever type is given to it.