A left value (lvalue) refers to an expression that can have its address taken using the & operator. These expressions typically appear on the left side of an assignment statement and represent identifiable objects in memory.
Examples of left values:
int i = 42; // i is an lvalue
int *p = &i; // i is an lvalue, address can be retrieved via &
int& demoFunction() {
return i;
}
demoFunction() = 42; // demoFunction returns an lvalue reference
int *p1 = &demoFunction(); // Valid usage of lvalue reference
Lvalues generally denote an object's identity and memory location. They are usually modifiable, although const lvalues should not be altered.
Characteristics of lvalues:
- Addressability: Lvalues can be addressed with the
&operator. - Assignability: They can appear on the left side of an assignment.
Types of lvalues include:
- Variable names (e.g.,
int x;) - Array elements (e.g.,
arr[0]) - Member access (e.g.,
obj.member) - Dereferenced pointers (e.g.,
*ptr)
Right Values
In C++, a right value (rvalue) represents a temporary object or literal that does not have a persistent memory address. These typically appear on the right side of an assignment and are ephemeral.
Rvalues often involve literals, temporary objects, or results of expressions:
// Literals are rvalues
int a = 10;
char b = 'A';
// Function return value is an rvalue
int c = generateResult(20, 10);
// Expression result is an rvalue
int m = a + b;
// String literal is an rvalue
const char *pName = "hello world";
// nullptr is an rvalue
int32_t *p = nullptr;
DemoClass obj = DemoClass();
Not all function return values are rvalues; they may also be lvalues if a reference is returned:
int& testLValueFunction() {
int i;
return i;
}
int testRValueFunction() {
int i = 5;
return i;
}
{
testLValueFunction() = 10; // Valid - returns lvalue reference
int *p1 = &testLValueFunction();
}
{
// testRValueFunction() = 10; // Invalid - returns rvalue
}
Left and Right Value References
In C++, references act as aliases for variables. A left value reference binds to lvalues, denoted by &. A right value reference binds to rvalues, denoted by &&.
C++11 introduced rvalue references to support move semantics and perfect forwarding.
class DemoClass {...};
// Accepts lvalue reference
void foo(X& x);
// Accepts rvalue reference
void foo(X&& x);
X x;
foo(x); // Calls foo(X&)
X bar();
foo(bar()); // Calls foo(X&&)
Overloading functions for both lvalue and rvalue references allows distinct behavior for each type:
void foo(const X& x); // Accepts both lvalue and rvalue
void foo(X&& x); // Only accepts rvalue
X x;
foo(x); // Invokes const X& version
X bar();
foo(bar()); // Invokes X&& version
Declaring rvalue references:
int a = 10;
int &lvalue_ref = a; // lvalue reference
int &&rvalue_ref = 10 + 20; // rvalue reference
Rvalue References and Move Constructors
Consider a container class holding a pointer to a large object. Copying this object incurs high overhead. Implementing a move consturctor avoids unnecessary copying:
class BasicClass {
public:
BasicClass() { std::cout << "construct\n"; }
~BasicClass() = default;
BasicClass(const BasicClass& ref) {
std::cout << "copy construct\n";
}
};
class ContainerClass {
private:
BasicClass *p = nullptr;
public:
ContainerClass() { p = new BasicClass(); }
~ContainerClass() { delete p; }
ContainerClass(const ContainerClass& ref) {
std::cout << "copy construct\n";
p = ref.p;
}
ContainerClass& operator=(const ContainerClass& ref) {
std::cout << "assignment\n";
BasicClass* tmp = new BasicClass(*ref.p);
delete this->p;
this->p = tmp;
return *this;
}
// Move assignment operator
ContainerClass& operator=(ContainerClass&& rhs) noexcept {
std::cout << "move assignment\n";
std::swap(this->p, rhs.p);
return *this;
}
};
When assigning a temporary object, the move assignment is invoked instead of the copy assignment:
static ContainerClass createObject() {
return ContainerClass();
}
{
ContainerClass p;
p = createObject(); // Uses move assignment
}
std::move and Move Semantics
The std::move function converts an lvalue into an rvalue, enabling move operations:
{
ContainerClass p;
ContainerClass q;
p = std::move(q); // Move q into p
}
{
ContainerClass p;
p = std::move(createObject()); // Move temporary into p
}
Universal References
Universal references enable perfect forwarding in templates:
template<typename T>
void foo(T&& param);
int x = 27;
const int cx = x;
const int& rx = cx;
foo(x); // T is int&, param is int&
foo(cx); // T is const int&, param is const int&
foo(rx); // T is const int&, param is const int&
foo(27); // T is int, param is int&&
std::forward for Perfect Forwarding
std::forward preserves the value category of forwarded arguments:
template<typename T, typename Arg>
std::shared_ptr<T> factory_v4(Arg&& arg) {
return std::shared_ptr<T>(new T(std::forward<Arg>(arg)));
}
// Equivalent to:
// shared_ptr<a> factory_v4(int& arg) { return shared_ptr</a><a>(new A(std::forward<int>(arg))); }
// shared_ptr<a> factory_v4(int&& arg) { return shared_ptr</a><a>(new A(std::forward<int>(arg))); }
</int></a></int></a>
Return Value Optimization
Modern compilers perform Return Value Optimization (RVO), wich avoids unnecessary copies:
BasicClass foo() {
BasicClass x;
return x; // No copy due to RVO
}
BasicClass bar() {
BasicClass x;
return std::move(x); // Still optimized due to RVO
}
Safe Move Operations
When containers like std::vector reallocate memory, they prefer move constructors if available:
- If a move constructor is
noexcept, it's used for efficient transfer. - Otherwise, the copy constructor is used.