References, parameter passing, returns
L-Values and R-Values
Recall that there is a difference between the concept of an Lvalue and an
Rvalue. These get their names from the types of items that can go on the
left-hand-side and right-hand-side of an assignment statement.
- lvalue -- an expression that identifies a non-temporary object. This is
a changeable storage location. Typical examples include named variables or array/vector
locations
- rvalue -- an expression that identifies a temporary object or a value not
associated with a named object (like a literal or the result of a calculation)
- Generally, an rvalue exists only during the statement in which it appears, and will be
deallocated/reclaimed memory after the statement or context is over
Examples:
int x, y; // L-values
int list[10]; // L-value
list[5] = 20; // list[5] is an L-value (a storage location in the named array)
int z = x + y; // x + y is an R-value. z is an L-value
string str = "Hello"; // str is L-value. "Hello" is R-value
// Assume Fraction is a class
Fraction f; // L-value
Fraction g(1,2); // L-value
g = Fraction(3,4); // this direct constructor call creates an R-value
// which is assigned into g with assignment operator
L-values can be used in R-value positions, when we are just accessing
the stored value.
x = y; // y is L-value, but can be used in R-value position
Lvalue References
The standard reference variable you've learned about previously is now referred to in the
C++11 standard as an Lvalue reference. The following notes page from Programming 1
illustrates the usual use of this type of reference variable:
The most important use of reference variables is in parameter
passing. Most notably the choices between:
- Pass by value -- makes a local copy of passed argument. Accepts R-values
- Pass by reference -- does not make a copy, allows original argument to be
changed. Only accepts L-values
- Pass by const reference -- does not make a copy, prevents original argument from
change. Accepts R-values
The usual decision factors for choosing how to pass parameters are:
- Do we want the formal parameter to be able to change the original argument? If yes,
choose pass by reference
- Otherwise, is this a small object with little copy overhead? (If so, pass by
value is fine).
Or is this a large object with large or complex copy overhead? (If
so,
best to use pass by const reference)
Other uses for L-value references
Remember that a reference variable acts as a nickname, or synonym, for the object it
references. This comes in handy in a few other places besides parameter passing
Aliasing complex names
- Suppose we have the following object:
vector<string> myList; // a vector of strings, called myList
- Since vectors overload the bracket operator [], we could end up with some complicated
index location based on a function call, like this:
myList[ indexChoice(x, myList.size() ) ]
- What if this choice of location were being used in some other calls in multiple places?
By using a reference for the above, we can get simpler statements. We can even combine with the
auto keyword to auto-detect the type, if it's not a simple type.
string & whichItem = myList[ indexChoice(x, myList.size() ) ];
// (or)
auto & whichItem = myList[ indexChoice(x, myList.size() ) ];
- This would allow easier usage in a more complicated later call, like this:
if ( check1(whichItem) && check2(whichItem) && whichItem.length() < 100)
whichItem.push_back('X');
whichItem is used 4 times in the above sample code. This is much easier than
writing
out and evaluating myList[ indexChoice(x, myList.size() ) ] 4 times.
- Also note that making this a reference variable in the above example
was crucial, because push_back() is a mutator function of the
string class. If whichItem were a value variable, we would have
inserted data into a copy, not the original.
Use in range-based for loops
- Range-based for loops are nice shorthand for operations that will apply to
every element of an array or collection. But a value variable in the range-based for
will be a copy of the data from the collection:
int array[10] = {2, 4, 6, 8, 10};
// Good usage. Will print each array element
for (int x : array)
cout << x << '\n';
// Bad usage. Attempts to increment each array slot, but only affects x, a copy
for (int x : array)
++x;
- Use a reference variable as the iteration variable when you want to change collection
elements:
// This fixes the prior bad attempt
for (int & x : array)
++x;
Avoiding unnecssary copies
Return-by-value vs Return-by-reference
Note that function returns have the same options as the parameter passing list:
- Return by value -- makes copy of the value returned (can be inefficient on
large objects)
- Return by reference -- no copy
- Return by const reference -- no copy, caller cannot use to modify the item
However, be very careful when returning a reference! Make sure you are returning something
that will still live past the function's execution! See these two sample functions:
// WHICH of these two functions is a GOOD definition?
// and WHICH of these uses a reference return badly?
const string & findMax(const vector<string> & list)
{
int maxIndex = 0;
for (int i = 1; i < list.size(); i++)
if ( list[maxIndex] < list[i])
maxIndex = i;
return list[maxIndex];
}
const string & findMax(const vector<string> & list)
{
string maxValue = list[0];
for (int i = 1; i < list.size(); i++)
if (maxValue < list[i])
maxValue = list[i];
return maxValue;
}
R-value references
c++11 introduced a new kind of reference variable -- an r-value reference
- To declare one, use && after a type
int & // type designation for an L-value reference
int && // type designation for an R-value reference
- L-value references can only refer to L-values
- R-value references can reference to R-values (temporaries)
int x, y, z; // regular variables
int & r = x; // L-value reference to the variable x
int & r2 = x + y; // This would be ILLEGAL, since x + y is an R-value
int && r3 = x + y; // LEGAL. R-value reference, referring to R-value
- More examples:
string str = "Hello";
string & rstr = str; // reference to str
rstr += " World"; // str is now "Hello World"
string & bad1 = "hello"; // ILLEGAL: "hello" is not lvalue
string & bad2 = str + " leader"; // ILLEGAL: str + " leader" is not lvalue
string & bad3 = str.substr(0,4); // ILLEGAL: call to function does not return lvalue
// HOWEVER
string && bad1 = "hello"; // LEGAL -- right side is rvalue
string && bad2 = str + " leader"; // LEGAL
string && bad3 = str.substr(0,4); // LEGAL
R-value references as parameters
- While it may not be immediately obvious why one would want to do this, r-value
references create a 4th type of parameter passing. Example:
void Func(const int & x); // pass by const reference (Lvalue)
void Func(int && x); // pass by r-value reference
- Normally, the first of these functions will accept R-values to be passed as well.
But if we add the second function, this one will accept R-values to be passed, by
R-value
reference.
int x = 10;
Func(x); // calls first function
Func(x + 5); // calls second function (r-value)
- Try this example to illustrate
the difference in this syntax
- There is also a std::move function in the <utility> library, which
will cast the parameter as an r-value reference:
vector<string> list; // vector of strings
vector<string> && x = std::move(list); // returns r-value reference to list
This is easier than doing it manually with a type cast:
vector<string> && y;
y = static_cast<vector<string> && >(list); // type cast version
This will be used in examples below.
Why? What's the point?!
Well, I'm glad you asked... :-)
- The point is to help instantiate move semantics
- In many cases, we end up copying r-values (temporaries) into more permanent storage
variables.
- For small objects, especially those not involving internal pointers or dynamic
allocation, this copy is not expensive. No problem
- What about more expensive copy situations? (dynamic allocation, large objects,
etc)
- move semantics, through r-value references, can allow us to swap resources with
the temporary r-value, which is about to be destroyed anyways
- To do this, we need to be able to change the temporary r-value! Hence, r-value
references!
- Consider this kind of swap function:
void swap(int & x, int & y)
{
int temp = x; // copy x into temp
x = y; // copy y into x
y = temp; // copy temp into y
}
We are making three copies to swap two things. Since these are ints, this is pretty
inexpensive. But what about this one?
void swap(vector<string> & x, vector<string> & y)
{
vector<string> temp = x; // copy x into temp
x = y; // copy y into x
y = temp; // copy temp into y
}
Not so efficient. Copying a vector is expensive, and this does it three times.
- The idea behind a move is to have these two vectors trade resources (which would
involve swapping their internal pointers), rather than make the tedious 3 copies above. This
will happen if we:
- do the swap based on r-value references (convert with std::move, from
<utility>)
- are swapping a type that appropriately incorporates a "move constructor" and "move
assignment operator
void swap(vector<string> & x, vector<string> & y)
{
vector<string> temp = std::move(x); // move x's resource to temp
x = std::move(y); // move y's resources to x
y = std::move(temp); // move temp's resources to y
}
Note that the std::vector class library already has appropriate constructors and
assignment operators to incorporate move semantics
- More generically, there is a std::swap function (also from
<utility>) that does this as a template function:
template <class T> void swap (T& a, T& b)
{
T c(std::move(a)); a=std::move(b); b=std::move(c);
}
Move copy constructor and assignment operator
- You are probably already familiar with these automatic member functions, found in every
class:
- Destructor
- Copy constructor
- Assignment operator
- Recall that we need to write these (destructor for dynamic clean-up, copy constructor and
assignment operator for "deep copy") whenever a class has a pointer or reference as member
data, typically when using dynamic memory management inside the class.
- In C++11, we have two extras, making this big five:
- Destructor
- Copy constructor
- Move constructor
- Copy assignment operator
- Move assignment operator
- The extra two (move constructor and move assignment operator) serve the same
general purpose as the regular copy constructor and assignment operator, but they do their work
using move semantics.
- Move constructor is used when an R-value is the original being used to construct a new
object
- Move assignment operator is used when an R-value is the right-hand-side of an assignment
into an object
- These allow the object to trade internal resources with the r-value (temporary), which is
about to be deallocated
- Saves on excessive copy overhead when not needed
- We do not want these to run when original (or right-side) is an L-value, like a
named object -- because that object will still exist after. Copy is needed in that case
(hence copy constructor and copy assignment operator are still relevant)
Here is an example to help demonstrate the move copy
constructor and move assignment operator. This is the start of a simple non-templated vector
class that stores a list of integers.
The protypes of the big five for this class:
~IntVector(); // destructor
IntVector(const IntVector& rhs); // copy constructor
IntVector(IntVector&& rhs); // move constructor
IntVector& operator=(const IntVector& rhs); // copy assignment operator
IntVector& operator=(IntVector&& rhs); // move assignment operator