The Notion of Lvalues and Rvalues






4.83/5 (24 votes)
This article aims at consolidating the less talked concept of Lvalues and Rvalues
Introduction
Before parsing through the Rvalue
references draft in C++11 standard, I never took Lvalue
s and Rvalue
s seriously. I even never overhead them among my colleagues or read about them in any C++ books (or may be I would have skipped that part thinking it to be of no importance). The only place I find them often is in compilation errors, like : error C2106: '=' : left operand must be Lvalue
. And just by looking at the statement/expression that generated this error, I would understand my stupidity and would graciously correct it with no trouble.
int NextVal_1(int* p) { return *(p+1); }
int* NextVal_2(int* p) { return (p+1); }
int main()
{
int a[] = {1,2,3,4,5};
NextVal_1(a) = 9; //Error. left operand must be l-value
*NextVal_2(a) = 9; // Fine. Now a[] = {1,9,3,4,5}
}
I hope with the above code you got what I am saying.
When I went on to read that RValue
reference section of C++0x, my vision and confidence started shaking a bit. What I took for granted as Lvalue
s started appearing as Rvalue
s. In this article, I will try to simplify and consolidate various concepts related to L & R values. And I feel it necessary to upload this article first, before updating C++11 – A Glance [part 1 of n]. I promise to update it also soon.
Please note that this effort mainly involves gathering scattered information and organizing it in a simple form so that one may not need to Google it again. All credits go to the original authors.
Definitions
An object can be viewed as a region of storage and this storage region can either be just observable or modifiable or both depending on the access specifier associated with it. What I mean is:
int i; // Here the storage region related to i is both
// Observable and Modifiable
const int j = 8; // Here the storage region related to j is only Observable
// but NOT Modifiable
Before proceeding to the definitions, please memorize this phase: "The notion of Lvalueness or Rvalueness is solely on the expression and nothing to do with the object." Let me simplify it:
double d;
Now d
is just an object of type double
[and thrusting l/r valueness upon d
at this stage is meaningless]. Now once this goes into an expression say like:
d = 3.1414 * 2;
then the whole concept of l/r values originates. Here, we are having an assignment expression with d
on one side and a numerical expression on another side which evaluates to a temporary value and will disappear after semicolon. The 'd
' which points to an identifiable memory location is an Lvalue
and (3.1414*2) which is a temporary is an Rvalue
.
At this point, let's define them
Lvalue
: An Lvalue
is an expression referring to an object, [which holds some memory location] [The C Programming Language - Kernighan and Ritchie]
Rvalue
: The C++ standard defines r-value by exclusion - "Every expression is either an Lvalue
or an Rvalue
." So an Rvalue
is any expression that is not an Lvalue
. To be precise, it is an expression that does not necessarily represent an object holding identifiable memory region (it may be temporary).
Points on Lvalues and Rvalues
- Numeric literals, such as
3
and3.14159
, areRvalue
s. So are character literals, such as 'a
'. - An identifier that names an enumeration constant is an
Rvalue
. For example:enum Color { red, green, blue }; Color enumColor; enumColor = green; // Fine blue = green; // Error. blue is an Rvalue
- The result of binary
+
operator is always anRvalue
.m + 1 = n // Error. As (m+1) is Rvalue.
- The unary
&
(address-of) operator requires anLvalue
as its operand. That is,&n
is a valid expression only ifn
is anLvalue
. Thus, an expression such as&3
is an error. Again,3
does not refer to an object, so it's not addressable. Although the unary&
requires anLvalue
as its operand, its result is anRvalue
.int n, *p; p = &n; // Fine &n = p; // Error: &n is an Rvalue
- In contrast to unary &, unary * produces an
lvalue
as its result. A non-null
(valid) pointerp
always points to an object, so*p
is anlvalue
. For example:int a[N]; int *p = a; *p = 3; // Fine. // Although the result is an Lvalue, the operand can be an Rvalue *(p + 1) = 4; // Fine. (p+1) is an Rvalue
- Pre-increment operator expressions results
LValue
s:int nCount = 0; // nCount represents a persistent object and hence Lvalue ++nCount; // This expression is an Lvalue as this alters // and then points to nCount object // Just to prove that this is an Lvalue, we can do the below operation ++nCount = 5; // Fine.
- A function call is an
Lvalue
if and only if the result type is a reference.int& GetBig(int& a, int& b) // returning reference to make the function call an Lvalue { return ( a > b ? a : b ); } void main() { int i = 10, j = 50; GetBig( i, j ) *= 5; // Here, j = 250. GetBig() returns the ref of j and it gets multiplied by 5 times. }
- A reference is a name, so a reference bound to an
Rvalue
is itself anLvalue
:int GetBig(int& a, int& b) // returning an int to make the function call an Rvalue { return ( a > b ? a : b ); } void main() { int i = 10, j = 50; const int& big = GetBig( i, j ); // Here, I am binding 'big' an Lvalue to the return value from GetBig(), an Rvalue. int& big2 = GetBig(i, j); // Error. big2 is not binding to the return value as big2 // is not const }
Rvalue
s are temporaries and don't necessarily point to a memory region but they may hold memory in some cases. It is not advisable to catch this address and do any further operations as it would be a booby trap to work on these temporaries.char* fun() { return "Hellow"; } int main() { char* q = fun(); q[0]='h'; // Exception is thrown here as fun() returns an temporary memory // and we are trying to access it !!! }
- Post-increment operator expressions results
RValue
s:int nCount = 0; // nCount represents a persistent object and hence Lvalue nCount++ // This expression is a Rvalue as it copies the value of the // persistent object, alters it and then returns the temporary copy. // Just to prove that this is an Rvalue, we can not do the below operation nCount++ = 5; //Error
By summarizing the above points, we can blindly state that: If we can take address of an expression (for further operations) safely, then it is a lvalue
expression, else it is an rvalue
expression. It makes sense right, as it is preposterous to carry on with a temporary.
Note: Both Lvalue
s and Rvalue
s could be modifiable or non-modifiable. Here are the examples:
string strName("Hello"); // modifiable lvalue
const string strConstName("Hello"); // const lvalue
string JunkFunction() { return "Hellow World"; /*catch this properly*/}//modifiable rvalue
const string Fun() { return "Hellow World"; } // const rvalue
Conversion between Lvalues and Rvalues
Can an Lvalue
appear in a context that requires an Rvalue
? YES, it can. For example:
int a, b;
a = 8;
b = 5;
a = b;
This =
expression uses the Lvalue
sub-expression b
as Rvalue
. In this case, the compiler performs what is called lvalue
-to-rvalue
conversion to obtain the value stored in b
.
Now, can an r-value appear in a context that requires an l-value. NO, it can't.
3 = a // Error. Here 3 which is an RValue is appearing in the context where
// Lvalue is required
Acknowledgments
Thanks to Clement Emerson for readily helping me in gathering and organizing this information.