|
| 1 | +#include <iostream> |
| 2 | +#include <memory> |
| 3 | +#include <utility> |
| 4 | + |
| 5 | +// This file contains the code used in the Spring2024 15-445/645 C++ bootcamp. |
| 6 | +// It dives deeply into C++ new features like move constructor/assign operator, move semantics, unique_ptr, |
| 7 | +// shared_ptr, wrapper class, etc., by implementing a simple version of unique_ptr from scratch. |
| 8 | + |
| 9 | +// **IMPORTANT NOTES**: |
| 10 | +// 1. please read `move_semantics.cpp` and `move_constructors.cpp` in `src` before reading this file! |
| 11 | +// 2. please BEGIN your reading from the MAIN function! |
| 12 | + |
| 13 | +// It is our implementation of std::unique_pointer<T>, and the real implementation is more complex! |
| 14 | +// A template allows us to replace any type T, with what we want later in our code. |
| 15 | +template <typename T> |
| 16 | +class Pointer { |
| 17 | + public: |
| 18 | + Pointer() { |
| 19 | + ptr_ = new T; |
| 20 | + *ptr_ = 0; |
| 21 | + std::cout << "New object on the heap: " << *ptr_ << std::endl; |
| 22 | + } |
| 23 | + Pointer(T val) { |
| 24 | + ptr_ = new T; |
| 25 | + *ptr_ = val; |
| 26 | + std::cout << "New object on the heap: " << val << std::endl; |
| 27 | + } |
| 28 | + // Destructor is called whenever an instance gets out of scope (just when the stack pops). |
| 29 | + ~Pointer() { |
| 30 | + if (ptr_) { |
| 31 | + std::cout << "Freed: " << *ptr_ << std::endl; |
| 32 | + delete ptr_; |
| 33 | + } |
| 34 | + } |
| 35 | + |
| 36 | + // Copy constructor is explicitly deleted. |
| 37 | + Pointer(const Pointer<T> &) = delete; |
| 38 | + // Copy assignment operator is explicitly deleted. |
| 39 | + Pointer<T> &operator=(const Pointer<T> &) = delete; |
| 40 | + |
| 41 | + // Add move constructor: useful when we need to EXTEND the lifetime of an object! |
| 42 | + Pointer<T>(Pointer<T> &&another) : ptr_(another.ptr_) { another.ptr_ = nullptr; } |
| 43 | + // Add move assign operator: useful when we need to EXTEND the lifetime of an object! |
| 44 | + Pointer<T> &operator=(Pointer<T> &&another) { |
| 45 | + if (ptr_ == another.ptr_) { // In case `p = std::move(p);` |
| 46 | + return *this; |
| 47 | + } |
| 48 | + if (ptr_) { // We must free the existing pointer before overwriting it! Otherwise we LEAK!! |
| 49 | + delete ptr_; |
| 50 | + } |
| 51 | + ptr_ = another.ptr_; |
| 52 | + another.ptr_ = nullptr; // NOTE: L14 avoids freeing nullptr during the destruction. |
| 53 | + return *this; |
| 54 | + } |
| 55 | + |
| 56 | + // Overload operator *, in order to make the Pointer<T> feel like a "pointer". |
| 57 | + // Note that the line below is an example of the following syntax we can use with our unique ptr type. |
| 58 | + // `p1.set_val(10)` -> `*p1 = 10` |
| 59 | + T &operator*() { return *ptr_; } |
| 60 | + |
| 61 | + T get_val() { return *ptr_; } |
| 62 | + void set_val(T val) { *ptr_ = val; } |
| 63 | + |
| 64 | + private: |
| 65 | + T *ptr_; |
| 66 | +}; |
| 67 | + |
| 68 | +// INCORRECT version of smart_generator |
| 69 | +template <typename T> |
| 70 | +Pointer<int> &dumb_generator(T init) { |
| 71 | + Pointer<T> p(init); |
| 72 | + return p; // NOOO! A DANGLING REFERENCE! |
| 73 | +} |
| 74 | + |
| 75 | +template <typename T> |
| 76 | +Pointer<T> smart_generator(T init) { |
| 77 | + Pointer<T> p(init); |
| 78 | + return std::move(p); |
| 79 | + // Actually `return p` will also work, since C++ is smart, it knows move construtor should be invoked in this place. |
| 80 | + // You can refer to `Automatic l-values returned by value may be moved instead of copied` in |
| 81 | + // https://www.learncpp.com/cpp-tutorial/move-constructors-and-move-assignment/ for more information. |
| 82 | +} |
| 83 | + |
| 84 | +void take_ownership(std::unique_ptr<int> p) { |
| 85 | + // Do something... |
| 86 | +} |
| 87 | + |
| 88 | +void not_take_ownership(int *p) { |
| 89 | + // Never `delete p` here!! |
| 90 | +} |
| 91 | + |
| 92 | +int main() { |
| 93 | + /* ====================================================================== |
| 94 | + === Part 1: Common errors you come across in bustub ================== |
| 95 | + ====================================================================== */ |
| 96 | + // When coding in C++/in this class, you will see a variable type called "unique_ptr"... |
| 97 | + std::unique_ptr<int> ptr = std::make_unique<int>(3); |
| 98 | + // What does this mean? Why we don't use the raw pointer like `int *p = new int`? (The answer is in Part 2) |
| 99 | + // And later, when you need to pass this unique_ptr to a function, you may write the following code (please |
| 100 | + // try to uncomment next line)... |
| 101 | + // take_ownership(ptr); |
| 102 | + // It doesn't work. The error is `Call to implicitly-deleted copy constructor of 'std::unique_ptr<int>'`. |
| 103 | + // You may search the Internet, and other people tell you to add a thing called `std::move`... |
| 104 | + take_ownership(std::move(ptr)); |
| 105 | + // It works! Looks great! And you continue coding... |
| 106 | + // Later, you may want to use p1 again (please try to uncomment next line)... |
| 107 | + // *ptr = 3; |
| 108 | + // Another error :(, and it says `segmentation fault`. |
| 109 | + // It looks confusing. What exactly happened? |
| 110 | + // We will explain it in this bootcamp, by implementing a simple version of unique_ptr from scratch! |
| 111 | + |
| 112 | + /* ====================================================================== |
| 113 | + === Part 2: Why we need unique_ptr rather than the raw pointer ======= |
| 114 | + ====================================================================== */ |
| 115 | + // What's the problem of merely using the raw pointer? |
| 116 | + int *p = new int; // Malloc |
| 117 | + *p = 456 * 12 / 34 + 23; |
| 118 | + if (*p == 76) { |
| 119 | + delete p; // You may forget to add this line, and have `memory leak` problem! |
| 120 | + return 0; |
| 121 | + } |
| 122 | + delete p; // Free |
| 123 | + |
| 124 | + // Raw pointers are dangerous! If you don't pay attention, you will come across problems like memory leaks, double |
| 125 | + // freeing, use after freeing... |
| 126 | + // The reason is that in C++, raw pointers have no inherent mechanism to clean up automatically! |
| 127 | + // Programmers have to allocate and deallocate heap memory all by themselves, and it is easy to go wrong. |
| 128 | + |
| 129 | + // We notice that, different from the memory in heap, the local variables in the stack will be created and deleted |
| 130 | + // automatically. Can we bind a raw pointer with a local variable in the stack? |
| 131 | + // It means, when this local variable is created, the heap memory for this raw pointer is automatically malloced. |
| 132 | + // And when this local variable dies, the raw pointer will be freed automatically. (For more details: search RAII) |
| 133 | + |
| 134 | + // Let's use C++ class to implement it! |
| 135 | + // Consider a class whose only job is to hold and "own" a raw pointer, and then deallocate that pointer when the class |
| 136 | + // object went out of scope... |
| 137 | + // This class is called `smart pointer`, and unique_ptr is one of smart pointers. |
| 138 | + // But, why we can't copy unique_ptr? What is std::move? |
| 139 | + |
| 140 | + /* ====================================================================== |
| 141 | + === Part 3: Let's implement a unique_ptr class from scratch ========== |
| 142 | + ====================================================================== */ |
| 143 | + // We only show the last version of our own unique_ptr class. |
| 144 | + // Here is the brief roadmap during the implementation: |
| 145 | + // 1. First version: with default copy constructor & assign operator, without move constructor & assign operator |
| 146 | + // Problem: `Pointer p2 = p1` will cause `double free` problem |
| 147 | + // Copy constructor & assign operator are evil in this case, since it will allow both p1 and p2 to manage the same |
| 148 | + // raw pointer! Solution: disable copy constructor & assign operator |
| 149 | + // 2. Second version: without copy constructor & assign operator, without move constructor & assign operator |
| 150 | + // `Pointer p2 = p1` won't compile, which is good. We can use reference `Pointer &p2 = p1` instead. But... |
| 151 | + // Problem: we cannot implement functions like dumb_generator() or smart_generator()! |
| 152 | + // Solution: add things called move constructor & assign operator |
| 153 | + // 3. Final version: without copy constructor & assign operator, with move constructor & assign operator |
| 154 | + // `Pointer p4 = std::move(p3);` |
| 155 | + // `std::move` guarantees this line of code invokes the `move constructor`(rather than `copy constructor`), to |
| 156 | + // transfer the ownership of the raw pointer from p3 to p4! |
| 157 | + // After this line, p3 will not be valid anymore! |
| 158 | + // The ptr_ in p3 will be nullptr, please don't use p3 anymore unless you reassign it. |
| 159 | + // Now you understand what is `std::move`, why copy functions are deleted... And how to use unique_ptr! |
| 160 | + // Reference: Chapter 22 in Learncpp Website |
| 161 | + // (https://www.learncpp.com/cpp-tutorial/introduction-to-smart-pointers-move-semantics/) |
| 162 | + Pointer<int> p1(4); |
| 163 | + std::cout << "Hi from p1 " << p1.get_val() << std::endl; |
| 164 | + p1.set_val(10); |
| 165 | + std::cout << "Hi again from p1 " << p1.get_val() << std::endl; |
| 166 | + |
| 167 | + { |
| 168 | + // Problem in next line: both have the ownership of this raw pointer! Double free here! |
| 169 | + // Pointer<int> p2 = p1; // Code for 1st version implementation. |
| 170 | + // Solution: never allow to copy ownership of the pointer! Never copy! |
| 171 | + // After deleting copy assign operator & constructor, maybe we can use pointer to rewrite `p2 = p1`. |
| 172 | + Pointer<int> *p2 = &p1; // Code for 2nd version implementation. |
| 173 | + std::cout << "Hi from p2 " << p2->get_val() << std::endl; |
| 174 | + // Wait this is dumb, we have a raw pointer again... Maybe we can use C++ reference, which is safer! |
| 175 | + // It's semantically the same thing with `Pointer<int> *p2 = &p1`, except the programmer doesn't **know** the pointer |
| 176 | + // (i.e. address of p2). |
| 177 | + Pointer<int> &p22 = p1; // Code for 2nd version implementation. |
| 178 | + std::cout << "Hi from p22 " << p22.get_val() << std::endl; |
| 179 | + } |
| 180 | + // But reference doesn't solve everything :( |
| 181 | + // Sometimes we want to use the heap to extend the scope of the stack, like what dumb_generator() does! |
| 182 | + // Ex: pass down one element from a thread to another. |
| 183 | + // Please try to uncomment the following code! |
| 184 | + // Pointer<int>& dumb_pointer = dumb_generator(2); // Something will go horribly wrong, but what? |
| 185 | + // dumb_pointer.set_val(10); // Uh oh... |
| 186 | + |
| 187 | + // We need a way to "move the ownership". Please check move assign operator/constructor in Pointer class. |
| 188 | + // And we change dumb_generator() to smart_generator()... |
| 189 | + // Code for final version implementation: |
| 190 | + Pointer<int> p3 = smart_generator<int>(2); |
| 191 | + p3.set_val(10); |
| 192 | + Pointer<int> p4 = std::move(p3); |
| 193 | + |
| 194 | + // Let's make the user experience better. |
| 195 | + // 1. Templates. |
| 196 | + Pointer<float> p5(5.1); |
| 197 | + std::cout << "Hi from float p5 " << p5.get_val() << std::endl; |
| 198 | + // 2. Operator overload. |
| 199 | + Pointer<char> c1('a'); |
| 200 | + *c1 = 'b'; |
| 201 | + std::cout << "Hi from char c1 " << *c1 << std::endl; |
| 202 | + |
| 203 | + // You may be confused about: |
| 204 | + // `Pointer<T> &&` (in the move constructor and assign operator) |
| 205 | + // VS |
| 206 | + // `Pointer<T> &` (in the copy constructor and assign operator) |
| 207 | + |
| 208 | + // You have 2 options now. First, consider it as a syntax to distinguish copy and move, and go straight to Part 4; |
| 209 | + // Second, here is a quick explanation: |
| 210 | + // 1. You need to know lvalue & rvalue. According to Abi, a simplified definition of lvalues is that lvalues are |
| 211 | + // objects that refer to a location in memory. Rvalues are anything that is not a lvalue. |
| 212 | + // 2. `Pointer<T> &&` is a rvalue reference, while `Pointer<T> &` is a lvalue reference. |
| 213 | + // 3. `std::move(p)` will cast p from a lvalue to something, for example, a rvalue. |
| 214 | + // 4. For `Pointer p2 = p1`, it will invoke copy constructor, since p1 is a lvalue. |
| 215 | + // 5. For `Pointer p2 = std::move(p1)`, it will invoke move constructor, since std::move(p1) is a rvalue. |
| 216 | + |
| 217 | + /* ====================================================================== |
| 218 | + === Part 4: Some important takeaways for unique_ptr & shared_ptr ===== |
| 219 | + ====================================================================== */ |
| 220 | + // Several important takeaways for unique_ptr: (Reference: https://www.learncpp.com/cpp-tutorial/stdunique_ptr/) |
| 221 | + // 1. Always use std::make_unique() to create a unique_ptr. |
| 222 | + std::unique_ptr<int> up{std::make_unique<int>(1)}; |
| 223 | + // Please avoid writing the following code! |
| 224 | + // int *rp = new int; |
| 225 | + // std::unique_ptr<int> up1{ rp }; |
| 226 | + // std::unique_ptr<int> up2{ rp }; // WRONG! |
| 227 | + |
| 228 | + // 2. Ways to pass std::unique_ptr to a function. |
| 229 | + not_take_ownership(up.get()); |
| 230 | + // Unique_ptr `up` is still valid here! |
| 231 | + take_ownership(std::move(up)); |
| 232 | + // Unique_ptr `up` cannot be used here! |
| 233 | + |
| 234 | + // Several important takeaways for shared_ptr: (Reference: https://www.learncpp.com/cpp-tutorial/stdshared_ptr/) |
| 235 | + // 0. Multiple shared ptrs can have the ownership of a raw pointer at the same time. |
| 236 | + // Shared_ptr will count the number of shared ptrs that own the same raw pointer, |
| 237 | + // and free the raw pointer **only if** count == 0. |
| 238 | + std::shared_ptr<int> sp1{std::make_shared<int>(1)}; |
| 239 | + { |
| 240 | + // You can use copy constructor & assign operator for shared_ptr. |
| 241 | + std::shared_ptr<int> sp2 = sp1; |
| 242 | + std::cout << "Count: " << sp1.use_count() << std::endl; // Output: 2 |
| 243 | + } |
| 244 | + std::cout << "Count: " << sp1.use_count() << std::endl; // Output: 1 |
| 245 | + // 1. Always make a copy of an existing std::shared_ptr. |
| 246 | + int *rp = new int; |
| 247 | + std::shared_ptr<int> sp3{rp}; |
| 248 | + // std::shared_ptr<int> sp4{ rp }; // WRONG! |
| 249 | + std::shared_ptr<int> sp4{sp3}; |
| 250 | + // 2. Always use std::make_shared() to create a shared_ptr. |
| 251 | + |
| 252 | + return 0; |
| 253 | +} |
0 commit comments