Yes, declaring the output operator << to take a const argument works for both lvalues (const and non-const) and rvalues.
1 2 3 4 5 6 7 8 9
|
/// O/P operator for class A
/// Accepts lvalue references (const and non-const)
/// and rvalue references to A.
ostream& operator<<(ostream& os, const A& a)
{
os << a.x;
return os;
}
|
Now, we can test the output operator << with both lvalue references (const and non-const) and rvalue references:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21
|
/// A instances
A a1 {1}, a5 {5}, a10 {10}, const ca9 {9};;
void testlvals()
{
cout << "a1 = " << a1 << endl;
cout << "a5 = " << a5 << endl;
cout << "a10 = " << a10 << endl;
cout << "a9 = " << ca9 << endl; /// const
}
A f()
{
return A {0};
}
void testrvals()
{
cout << "f() = " << f() << endl;
}
|
In seeking an explanation for why this works, I found the following in Stroustrup's TC++PL4:
"An rvalue reference can bind to an rvalue, but not to an lvalue. In this, an rvalue reference is exactly opposite to an lvalue reference.
[However,] an rvalue can bind to both an rvalue reference and a const lvalue reference."
Basically, an rvalue is a temporary and soon to disappear. If it were possible to assign an rvalue to an lvalue, it would be possible to modify the rvalue. However, the modified rvalue would then shortly disappear. Therefore the compiler prevents one from assigning an rvalue to an lvalue.
On the other hand, when an lvalue is declared const, it can't be modified; therefore even if an an rvalue were assigned to that lvalue, the rvalue can't be modified thru the lvalue. Therefore, the compiler can allow such an assignment.
This is another reason that justifies the importance of "const correctness".
Basically, a function should take only const parameters, unless the function needs to modify the argument.
Further, it is a best practice that a function shouldn't modify arguments at all. If an argument is to be modified, it should be by the function's return value. This is from a readability / understandability viewpoint.
Keeping these 2 points in mind, we may formulate a best practice as :
"A FUNCTION SHOULD ALWAYS BE DECLARED TO TAKE ONLY const PARAMETERS."
The only exception is where the function takes a parameter that it intends to modify and then return as the return value.
An excellent example in fact is the output operator itself, which takes a non-const ostream& reference which it modifies and then returns:
1 2 3 4 5 6 7 8 9
|
/// O/P operator for class A
/// Accepts lvalue references (const and non-const)
/// and rvalue references to A.
ostream& operator<<(ostream& os, const A& a)
{
os << a.x;
return os;
}
|
Following are the benefits when a function parameter is declared const:
1) The compiler ensures the function can't modify the argument.
2) The programmer understands immediately that the function can't modify the argument.
3) non-const lvalues can be passed to the parameter.
4) const lvalues can be passed to the parameter.
5) rvalues can be passed to the parameter. All rvalues are non-const. There is no such thing as a const rvalue, since an rvalue permits a "destructive read".
Naturally, the same treatment also applies to constructors.