0%

Value Categories and Move Semantics in C++

This is my learning notes from the course - Mastering C++ Standard Library Features.

Lvalues and Rvalues

Why to talk about Lvalues and Rvalues in C++?

Knowledge of value categories and references is necessary to understand move semantics and ownership transfer.

Move Semantics - Sneak Peek

  • Move semantics revolve around the idea of transferring ownership of resources instead of copying them.
  • They increase performance, safety, and readability of libraries and applications.
1
2
3
4
5
6
7
// C++03
std::vector<data> v0;
std::vector<data> v1 = v0;

// C++11 and later
std::vector<data> v0;
std::vector<data> v1 = std::move(v0);

As an example, in C++03, if you have a vector v0 containing some data and another vector v1, and you want to move all the data into v1, you had no way of expressing the idea of transferring the resource from v0 to v1, so you were forced to make a copy. In C++11 and later, we have this new library function called std::move that allows you to express the intent of moving the contents of v0 into v1, resulting in safer and more efficient code. Understanding value categories and Lvalues and Rvalues is the Key to understanding how std::move works and what move semantics are.

Value Categories

Lvalues

  • Can appear on the left side of built-in assignment operator
  • Can take its address
  • Can bind to lvalue references
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
int& bar() { static int i = 0; return i; }

int main()
{
int a;

// Lvalues can appear on the `left` side of the built-in assignment operator:
a = 0;

// The address of LValues can be taken:
int *a_ptr = &a;

// Lvalues can bind to Lvalue references:
int &a_ref = a;

// Example of Lvalues:
// (*) Name of variable:
a;

// (*) "Member of object" expression:
struct foo { int _b; };
foo f;
f._b;

// (*) Function call returning Lvalue reference:
int &bar();
&bar();
bar() = 5;

return 0;
}

Rvalues

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
int bar() { return 5; }

int main()
{
// Rvalues can NOT appear on the left side of the built-in assignment operator:
5 = 0;
bar() = 0;

// The address of Rvalues can NOT be taken:
&5;
&bar();

// Rvalues do NOT bind to Lvalues references:
int &lv_ref0 = 5;
int &lv_ref1 = bar();

// Rvalues bind to Rvalue references, introduced in C++11
int &&rv_ref0 = 5;
int &&rv_ref1 = bar();

// Examples of Rvalues
// (*) Numeric literals:
5;
10.33f;

// (*) Built-in arithmetic expression:
5 + 10 * 3;

// (*) Function calls returning non-references:
bar();

return 0;
}

References

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
#include <iostream>

void foo(int&) // takes an non-const `Lvalue` reference
{
std::cout << "non-const Lvalue ref\n";
}

void foo(int&&) // an overload of foo() that takes a non-const `Rvalue` reference
{
std::cout << "non-const Rvalue ref\n";
}
void bar(const int&) // takes a const `Lvalue` reference
{
std::cout << "const Lvalue ref\n";
}

int main()
{
int a = 0;

foo(a); // call foo(int&)
foo(5); // call foo(int&&)

bar(a); // call bar(const int&)
bar(5); // call bar(const int&)
}

Summary

  • C++ expressions have a value categories; they can either be Lvalues or Rvalues
  • Lvalues can appear on the left side of built-in assignment, Rvalues cannot
  • Non-const Lvalue references can only bind to Lvalues
  • Non-const Rvalue references can only bind to Rvalues

Move Semantics

Meaning of Rvalues

An rvalue represents a temporary object that has no identity. We can assume that an rvalue is ready to give away ownership of its resources.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
#include <vector>

std::vector<int> get_a_vector(){ return {1, 2, 3, 4, 5}; }

int main()
{
std::vector<int> v0{1, 2, 3, 4, 5};

// The Lvalue `v0` is an `std::vector` which owns a dynamically allocated buffer.

auto v1 = v0;

// If we instantiate a new vector `v1` and initialized it with the Lvalue `v0`,
// we are forced to perform a copy of `v0`'s internal buffer.

// An alternative consists of releasing the ownership of the source vector's
// buffer and giving it away to the destination.

// When can that be done safely? Only when the source is an Rvalue, as it doesn't
// have an "identity" and it is about to "expire".

auto v2 = get_a_vector();

// In this case, `get_a_vector()` is an Rvalue expression - we know that
// the returned vector has no "identity" and is about to "expire" -
// therefore we can give away the ownership of its internal buffer
// instead of performing a copy of all its elements.

// why?
// http://en.cppreference.com/w/cpp/container/vector/vector

return 0;
}

What are Move Semantics?

  • Transferring ownership of resources instead of copying them
  • Relying on Rvalue references
  • Colloquially express the idea of “moving” objects

std::move

  • It’s literally a cast to an Rvalue reference
  • Doesn’t actually “move” anything, but expresses “intent to move”
  • Can be used to turn an Lvalue into an Rvalue
  • It is generally unsafe to use an object after it has been moved
  • Moving when returning is unnecessary and sometimes detrimental
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
#include <type_traits>
#include <utility>
#include <vector>

int main()
{
// Let's now analyze a different scenario: we have an Lvalue `v0` that we
// want to "move" to a destination vector.

std::vector<int> v0{1, 2, 3, 4, 5};

// Assuming we know that `v0` won't be used anymore in the current scope
// and that an unnecessary copy can be avoided,
// how can we inform the compiler of our intentions?

auto v1 = std::move(v0);

// `std::move` does precisely that: it casts an existing Lvalue expression
// to an Rvalue, so that the implementation of `std::vector` can take
// advantage of the fact that `v0` is about to "expire".

// How does `std::move` work? It literally is only a `static_cast` to
// an rvalue reference.

// http://en.cppreference.com/w/cpp/utility/move

auto v2 = static_cast<std::vector<int>&&>(v1);

// In the line above, we perform the same operation `std::move` does,
// albeit in a more verbose and less expressive manner.

// Note that using `v0` or `v1` here would lead to undefined behavior.

return 0;
}

// One very important thing to understand is that `std::move` doesn't actually
// "move" anything. It simply casts an expression to an Rvalue.

// Think of `std::move` as a way of telling the compiler that "we would like to
// move this particular object if possible, and we swear that we're not going to
// use it again later".

void noop_example()
{
std::vector<int> v0{1, 2, 3, 4, 5};

std::move(v0); // No-op

v0.size(); // perfectly safe
}

// Another thing to be aware of is that using `std::move` when returning
// from a function is unnecessary and sometimes detrimental,
// as it prevents RVO ("Return Value Optimization").

std::vector<int> return_example()
{
std::vector<int> v0{1, 2, 3, 4, 5};

// Wrong:
// return std::move(v0);

// Correct:
return v0;
}

// This doesn't apply to classes though.
// RVO applies only to local variables and doesn't apply to something like a data member,
// because when we say return v here (below), this actually return `this->v`
// because `this` is the pointer to the local instance and that's a data member.

// So, in this case if we want to provide functionality that moves from a class instance
/// and moves a data member, it is appropriate to add `std::move` here.

struct foo
{
std::vector<int> v;

std::vector<int> move_v()
{
// Wrong:
// return v;

// Correct:
return std::move(this->v);
}
};

At line 16, the interesting part of this operation, the transferring of the ownership of the resource inside the vector happens in the vector’s move constructor implementation. std::move doesn’t have to do anything fancy. It is, again, just a cast, meaning that std::move doesn’t actually move anything.

One important thing is that using a value after it has been moved can lead to undefined behavior. That’s because implementation of the move constructor or move assignment, or any other move operation, assumes that the object is about to expire and it may leave the object in a valid, but unspecified state.

Summary

  • Learned that the Rvalue expressions represent temporariness and lack of “identity”
  • Learned that the std::move is just a cast to an Rvalue that expresses the “intention” of moving
  • Understood that it is unsafe to use objects after they have been “moved from”
  • Learned that moving when returning is unnecessary and sometimes detrimental

Reference Links