Based mostly on Thomas Becker's article
http:
What is the rvalue reference and why we need it?
I many earlier languages assignment works in the following way:
<variable> := <expression>
like
x := y + 5;
In C/C++ however there might be expressions on the left side of =
<expression> = <expression>
like
*++p = *++q;
But not all kind of expressions may appear on the left hand side:
y + 5 = x;
"Left value" is originated to C language: an l value is an expression
that may appear on the left hand side of an assignment. "Right value"
is anything else.
Basically left-value identified a writeable memory location in C.
In C++ this is a bit more complex, but here is a widely acecpted
definition:
An "lvalue" is an expression that refers to a memory location and allows
us to take the address of that memory location via the & operator.
An "rvalue" is an expression that is not an "lvalue"
These are lvalues
int i = 42;
int &j = i;
int *p = &i;
i = 99;
j = 88;
*p = 77;
int *fp() { return &i; }
int &fr() { return i; }
*fp() = 66;
fr() = 55;
These are rvalues
int f() { int k = i; return k; }
i = f();
p = &f();
f() = i;
A rigorous definition of lvalue and rvalue:
http:
C and C++ has value semantics -- when we use assignment we copy by default.
Array a, b, c, d, e;
a = b + c + d + e;
This will generate the following pseudo-code:
double* _t1 = new double[N];
for ( int i=0; i<N; ++i)
_t1[i] = b[i] + c[i];
double* _t2 = new double[N];
for ( int i=0; i<N; ++i)
_t2[i] = _t1[i] + d[i];
double* _t3 = new double[N];
for ( int i=0; i<N; ++i)
_t3[i] = _t2[i] + e[i];
for ( int i=0; i<N; ++i)
a[i] = _t3[i];
delete [] _t3;
delete [] _t2;
delete [] _t1;
Performace problems
- For small arrays new andi delete result poor performance: 1/10 of C.
- For medium arrays, overhead of extra loops and memory access add +50%
- For large arrays, the cost of the temporaries are the limitations
This has been investigated by Todd Veldhuizen and has led to C++ template
metaprogramming and expression templates.
It would be nice not to create the temporaries, but steal the resources of
the arguments of operator+()
But!
We can destroy only the temporaries: we should keep the original resources
for the b, c, d, e variables. How can we distinguis between variables and
unnamed temporaries? --> overloading
Overloading --> We need a separate type!
What kind of requirements we have for this type?
1. should be a reference type - otherwise we gain nothing
2. if there is an overload between ordinary reference and this new type
then rvalues should prefer the new type and lvalues the ordinary reference
Rvalue reference:
X &&
Old kind of references now are called as lvalue references:
X &
void f(X& arg_)
void f(X&& arg_)
X x;
X g();
f(x);
f(g());
We can overload copy constructor and assignmnet operator overloads:
class X
{
public:
X(const X& rhs);
X(X&& rhs);
X& operator=(const X& rhs);
X& operator=(X&& rhs);
private:
};
X& X::operator=(const X& rhs)
{
return *this;
}
X& X::operator=(X&& rhs)
{
return *this;
}
Reverse compatibility
If we implement the old-style memberfunctions with lvalue reference parameters
but do not implement the evalue reference overloading versions we keep the
old behaviour -> we can gradually move to move semantics.
However, if we implement "only" the rvalue operations than we cannot call
these on lvalues -> no default lvalue-copy constructor or operator= will
be generated.
std::move()
Can we use move semantic on lvalue Do we need this
template<class T>
void swap(T& a, T& b)
{
T tmp(a);
a = b;
b = tmp;
}
X a, b;
:
swap(a, b);
By default, this swap implementation will use copy semantic.
To enforce move semantic we should use std::move().
template<class T>
void swap(T& a, T& b)
{
T tmp(std::move(a));
a = std::move(b);
b = std::move(tmp);
}
X a, b;
:
swap(a, b);
std::move() converts its argument to rvalue reference, does not do
anything else. We should think std::move as "rvalue reference cast".
"The committee shall make no rule that prevents C++ programmers from
shooting themselves in the foot."
First Amendment to the C++ Standard quoted by Thomas Becker
http:
Therefore we can use std::move() with extra care.
-- Usually std::move() has positive effect on performance,
as many standard algorithm utilize move semantics.
-- Sometimes we have to use std::move().
There are types moveable but not copyable:
std::unique_pointer
std::fstream
std::thread
X&& f();
void g(X&& arg_)
{
X var1 = arg_;
X var2 = f();
}
Since arg_ is declared as rvalue reference we might think that here
X::X(X&& rhs) will be called. But not always:
Things that are declared as rvalue reference can be lvalues or rvalues.
Anything has a "name" is an "lvalue" otherwise it is an "lvalue".
Therefore in the var1 example above X::X(const X& will be called),
and in the var2 example X::X(&&) is called.
Philosophy:
After X var1 = arg_; arg_ is still in scope and can be available.
X&& f();
void g(X&& arg_)
{
X var1 = arg_;
X var2 = f();
}
If-it-has-a-name-rule:
class Base
{
public:
Base(const Base& rhs);
Base(Base&& rhs);
:
};
class Derived : public Base
{
Derived(const Derived& rhs);
Derived(Derived&& rhs);
:
};
Derived(Derived const & rhs) : Base(rhs)
{
}
Derived(Derived&& rhs) : Base(rhs)
{
}
Derived(Derived&& rhs) : Base(std::move(rhs))
{
}