|
| 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. |
0 commit comments