Skip to content

Commit d6bd76e

Browse files
authored
Add the code used in Spring 2024 bootcamp (#17)
1 parent b506403 commit d6bd76e

File tree

5 files changed

+270
-2
lines changed

5 files changed

+270
-2
lines changed

.gitignore

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -38,4 +38,7 @@ build/
3838
todo.txt
3939

4040
# VSCode
41-
.vscode/
41+
.vscode/
42+
43+
# Cache
44+
.cache/

CMakeLists.txt

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -42,4 +42,7 @@ add_executable(rwlock src/rwlock.cpp)
4242
add_executable(wrapper_class src/wrapper_class.cpp)
4343
add_executable(iterator src/iterator.cpp)
4444
add_executable(auto src/auto.cpp)
45-
add_executable(namespaces src/namespaces.cpp)
45+
add_executable(namespaces src/namespaces.cpp)
46+
47+
# Compiling bootcamp demo code
48+
add_executable(s24_my_ptr src/spring2024/s24_my_ptr.cpp)

README.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,9 @@ reading the files on concepts you are unfamiliar about.
6262
- `condition_variable.cpp`: Covers `std::condition_variable`.
6363
- `rwlock.cpp`: Covers the usage of several C++ STL synchronization primitive libraries (`std::shared_mutex`, `std::shared_lock`, `std::unique_lock`) to create a reader-writer's lock implementation.
6464

65+
### Demo Code for 15-445/645 Bootcamp
66+
- `spring2024/s24_my_ptr.cpp`: Covers the code used in Spring 2024 bootcamp.
67+
6568
## Other Resources
6669
There are many other resources that will be helpful while you get accquainted to C++.
6770
I list a few here!

src/spring2024/s24_my_ptr.cpp

Lines changed: 253 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,253 @@
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+
}

src/wrapper_class.cpp

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -77,6 +77,12 @@ class IntPtrManager {
7777
// Move assignment operator for this wrapper class. Similar techniques as
7878
// the move constructor.
7979
IntPtrManager &operator=(IntPtrManager &&other) {
80+
if (ptr_ == other.ptr_) {
81+
return *this;
82+
}
83+
if (ptr_) {
84+
delete ptr_;
85+
}
8086
ptr_ = other.ptr_;
8187
other.ptr_ = nullptr;
8288
return *this;

0 commit comments

Comments
 (0)