Skip to content

Commit 332c8f5

Browse files
committed
docs: Add note about move and references
1 parent b0d57f4 commit 332c8f5

4 files changed

Lines changed: 179 additions & 0 deletions

File tree

Lines changed: 160 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,160 @@
1+
# Syntax Note - `data_structure/pool`
2+
3+
## Rvalue References and Move Semantics
4+
5+
### Why this feature exists
6+
7+
Before C++11, C++ only had lvalue references. This meant that when you wanted to pass an object to a function, you had to decide whether to pass it by value (which would involve copying the object) or by reference (which could lead to unintended side effects if the function modified the object).
8+
9+
In the `copy constructor` whcih with `Lvalue reference`, as shown in the example below, it will create a new object by copying the data from another object. This can be inefficient if the object is large, as it involves copying all of its data.
10+
11+
In contrast, the `move constructor` with `Rvalue reference` allows you to transfer ownership of the data from one object to another without copying. This is more efficient, especially for large objects, as it avoids the overhead of copying.
12+
13+
```c++
14+
class MyClass {
15+
public:
16+
// copy constructor
17+
MyClass(const MyClass& other) {
18+
// copy the data from other
19+
}
20+
21+
// move constructor
22+
MyClass(MyClass&& other) {
23+
// move the data from other
24+
}
25+
};
26+
```
27+
28+
#### Universal references
29+
30+
But this is not enough, because we also want to be able to write functions that can accept both lvalues and rvalues without having to write separate overloads for each. This is where `universal references` come in.
31+
32+
A universal reference is a special type of reference that can bind to both lvalues and rvalues, it will be `type-deduced` based on the value category of the argument passed to the function. Then it will do `reference collapsing` to determine the final type of the reference.
33+
34+
For example, in the function template below, `T&& arg` is a universal reference, **forwarding reference**.
35+
- If `arg` is an `lvalue` with type `E`, then `T` will be deduced as `E&`, and the type of `arg` will be `E& &&`, which collapses to `E&`.
36+
- If `arg` is an `rvalue` with type `E`, then `T` will be deduced as `E`, and the type of `arg` will be `E&&`, which is an rvalue reference.
37+
38+
```c++
39+
template <typename T>
40+
void foo(T&& arg);
41+
42+
int a = 10;
43+
44+
foo(a); // T is deduced as int&, arg is int& &&, which collapses to int&
45+
foo(10); // T is deduced as int, arg is int&&, which is an rvalue reference
46+
```
47+
48+
#### Reference collapsing
49+
50+
How to understand `int && &&` or `int& &&`? This is where reference collapsing comes in. The rules for reference collapsing are as follows:
51+
52+
- `T& &` collapses to `T&`
53+
- lvalue reference to an lvalue reference collapses to an lvalue reference final type
54+
- `T& &&` collapses to `T&`
55+
- lvalue reference to an rvalue reference collapses to an lvalue reference final type
56+
- `T&& &` collapses to `T&`
57+
- rvalue reference to an lvalue reference collapses to an lvalue reference final type
58+
- `T&& &&` collapses to `T&&`
59+
- rvalue reference to an rvalue reference collapses to an rvalue reference final type
60+
61+
For example:
62+
```c++
63+
template <typename T>
64+
void foo(T&& arg) {
65+
// ...
66+
}
67+
68+
int a = 10;
69+
int&& r = 30;
70+
71+
// T& & → T&: lvalue of type int
72+
// Passing an lvalue (a) to foo
73+
// T is deduced as int& (because a is an lvalue of type int)
74+
// arg is of type int& &&, which collapses to int&
75+
foo(a);
76+
77+
// same as above
78+
foo(r);
79+
80+
// Passing an rvalue (10) to foo
81+
// T is deduced as int (because 10 is an rvalue of type int)
82+
// arg is of type int&&, which is an rvalue reference without collapsing.
83+
foo(10);
84+
```
85+
86+
```c++
87+
using LRef = int&;
88+
using RRef = int&&;
89+
90+
int a = 10;
91+
92+
// deduced int& & → collapses to int& (lvalue reference)
93+
LRef& x = a;
94+
95+
// deduced int&& & → collapses to int& (lvalue reference)
96+
RRef& y = a;
97+
98+
// deduced int& && → collapses to int& (lvalue reference)
99+
LRef&& p = a;
100+
101+
// deduced int&& && → collapses to int&& (rvalue reference)
102+
RRef&& q = 10;
103+
```
104+
105+
#### Move semantics
106+
107+
In the move constructor, we can use `std::move` to indicate that we want to move the data from one object to another.
108+
And the `std::forward` is used in the function template to perfectly forward the argument to another function, preserving its value category (lvalue or rvalue).
109+
110+
```c++
111+
template <typename T>
112+
void foo(T&& arg) {
113+
// perfectly forward arg to another function
114+
bar(std::forward<T>(arg));
115+
}
116+
117+
int a = 10;
118+
119+
foo(std::move(a)); // T is deduced as int, arg is int&&, which is an rvalue reference
120+
```
121+
122+
123+
```c++
124+
class MyClass {
125+
public:
126+
// move constructor
127+
MyClass(MyClass&& other) noexcept {
128+
// move the data from other
129+
}
130+
};
131+
132+
MyClass a;
133+
134+
// 1. move constructor is called by explicitly calling std::move
135+
MyClass b = std::move(a);
136+
137+
// 2. a is an lvalue, so copy constructor is called
138+
MyClass c(a);
139+
140+
// 3. pass a temporary object, so move constructor is called
141+
MyClass d = MyClass();
142+
143+
// 4. function returns a temporary object, so move constructor is called
144+
MyClass makeObj() { return MyClass(); }
145+
MyClass e = makeObj();
146+
```
147+
148+
> The `noexcept` specifier in the move constructor indicates that the move constructor does not throw exceptions. So for the STL containers, if the move constructor is `noexcept`, they will prefer to use the move constructor over the copy constructor when resizing or rehashing.
149+
150+
### Mental model
151+
152+
153+
154+
### Trade-off and Trap
155+
156+
1. After moving from an object, the state of the object is unspecified but valid. It is a common mistake to use an object after it has been moved from, which can lead to undefined behavior if the object is not in a valid state.
157+
158+
2. move semantics can lead to performance improvements, but it can also lead to performance degradation if not used correctly. For example, if you move an object that is small and cheap to copy, it may be more efficient to copy it instead of moving it. (e.g. `int`, `double`, etc. trivially copyable types)
159+
160+
3. `noexcept` will lead to `std::terminate` being called if an exception is thrown, so it should only be used when you are sure that the function will not throw exceptions. If you mark a move constructor as `noexcept` but it can actually throw exceptions, it can lead to unexpected behavior and crashes.
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
# Syntax Note
2+
3+
4+
## `std::optional<T>`
5+
6+
### Why this feature exists
7+
8+
### Mental model
9+
10+
### Trade-off
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
# Syntax Note - `data_structure/pool`
2+
3+
## Variadic Templates
4+
5+
### Why this feature exists
6+
7+
### Mental model
8+
9+
### Trade-off

0 commit comments

Comments
 (0)