Rvalues & Lvalues, Whats the difference?
Rvalues & its use is a bit hard to grasp at first, which is why I will go over
them slowly and what they're used for, how to use them yourself, and hopefully
by the end you will have an understanding of what the difference between
RValues & Lvalues is, what each type is usefull for, and how to utilize them.
So, lets start shall we?
Lvalues are objects that are stored (They're life time extends past the current statement),
they have an address (You can use the Address-of operator on them).
Rvalues are values/objects that are not stored (They're life time do NOT extend past the current statement),
you cannot use the address-of operator on them.
If that was not clear, here is an example:
Code:
int x = 5; // Lvalue, stored untill the end of the current scope
int y = 7; // Same as 'x'
&x; // Totally valid line.
&y; // Same
x + y; // Rvalue, the result of x + y gets destroyed at the semicolon.
x + y = 10; // Invalid line, rvalue on the left hand side of an assignment.
int z = x + y; // Valid, rvalue on the right hand sife of an assignment.
45; // Rvalue, gets destroyed at semicolon.
You should now have a basic understanding of what Rvalues/Lvalues are, and the
difference between them.
Move Semantics & Perfect Forwarding
Move Semantics & Perfect forwarding are almost always
used together, these features must be implemented by yourself
as the programmer. (But the STL provides functions for making it easier & possible)
Lets start with Move semantics, as Perfect forwarding comes in the picture later.
Move semantics
should be used for every type you create (class), especially ones
that are copy heavy (E.g takes up alot of memory, multiple inner vectors & or pointers etc).
What move semantics does is take advantage of Rvalues, you remember how rvalues were
destroyed once they reach the end of the statement, well. Since its gonna be destroyed anyway,
why not gut it first? (swap its components with yours), essentially moving the memory from the rvalue
into the current instance of the class.
First lets go over some of the functions in the STL related to move semantics.
std::move(lvalue);
std::forward<T>(l/r value);
The simplest way to probably explain what they actually do, is by telling you what they dont do.
std::move does
not actually move anything.
std::forward does
not actually forward anything.
You can see them as a cast.
std::move will always cast its argument to an rvalue.
std::forward will conditionally cast its value to an rvalue.
What I mean by conditionally cast is that if the argument is already of type
rvalue it will cast it to an rvalue, if its an lvalue it will cast it to an lvalue. (You will realize why this is usefull later)
Now suppose Foo is class that holds some resource, say
_resource. This resource is big & very costly
to copy, lets say it is a
std::vector of type
T and contains
N elements.
Code:
Foo v1;
Foo v2(v1); // Copy constructor is called (Foo(const Foo& other))
Foo v3(std::move(v1)); // Move constructor is called (Foo(Foo&& other))
// When v1 reaches the semicolon it will be destructed, but before its destructed
// the contents of v3 is swapped with v1, meaning v3 holds everything v1 held.
Now this doesn't mean you should
always use move semantics, calling std::move on an lvalue
basically destructs everything the lvalue contained, because thats how rvalues work, they get destructed at the end
of the current statement for memory management (Created & Destroyed on the stack).
Now that I've showed you how std::move is used, I should probably show you how you
create your own move constructor, so it actually works.
Code:
// in class Foo
Foo(Foo&& other)
{
// Now we just neatly swap the items in other with our own.
std::swap(_resources, other._resources);
}
That was simple right?
The next part may confuse you a little at first, but its extremely usefull as you do dont have to
re-create all your functions twice for an lvalue version & rvalue version. (std::forward is our saviour!):
Suppose we have a class that holds a
std::vector of type
T with the name of
_vec.
Now in our class we have a function to add items to this vector, T may be a large user defined type, or similiar
making it cost performance to copy a std::vector of T, hence why we want to use perfect forwarding when possible.
Code:
template<typename _T>
void Add( _T&& element ) // when calling this function you should NOT do obj->Add<type> because
// perfect forwarding relies on type deduction using universal references (typename of name T with && becomes a
// universal reference)
{
// Now universal references can be lvalues or rvalues (see where I'm getting?)
_vec.push_back( std::forward<_T>(element) );
}