CS3211 Tutorial 3
Smart Pointers
(AY 25/26 Semester 2)
February 12, 2026
(Compiled by Benson, thanks to all past and current TAs!)
Join our telegram group!
https://t.me/+MqClT5ObVgxiMzRl
1 / 26
Admin Info
No tutorials next week! Please don’t show up I won’t either.
You may check your attendance records on my website.
2 / 26
Contents
Why Smart Pointers?
std::unique ptr
std::shared ptr
Common Pitfalls when using std::shared ptr
Implementing Shared Pointer
3 / 26
Contents
Why Smart Pointers?
std::unique ptr
std::shared ptr
Common Pitfalls when using std::shared ptr
Implementing Shared Pointer
4 / 26
Smart Pointers std::unique ptr
1 int main() {
2 std::unique_ptr<int> foo =
3 std::make_unique<int>(0);
4 use_foo(foo.get()); // foo*
5 }
1 int main() {
2 auto deleter = [](FILE* f) {
3 fclose(f);
4 }
5 auto bar = std::unique_ptr<FILE, decltype(deleter)> {
6 fopen("file.txt", "w"), deleter
7 };
8 use_bar(bar.get());
9 }
Link to godbolt
Turns non-RAII
types into RAII
types (delete the
resource when the
pointer goes out of
scope).
Unique Ownership
5 / 26
Smart Pointers std::shared ptr
1 auto foo = std::make_shared<int>(0);
2 auto trader_fn = [](std::shared_ptr<int> foo) {
3 ...
4 };
5 std::thread { trader_fn, foo }.detatch();
6 std::thread { trader_fn, foo }.detatch();
Link to godbolt
Protects the shared
resource!
delete only after
the end of all
shared pointer’s
lifetime.
6 / 26
Smart Pointers std::shared ptr
1 void foo(std::shared_ptr<int> arg) {
2 std::cout << *x;
3 }
4
5 int main() {
6 auto x = std::make_shared<int>(100);
7 foo(x);
8 }
Stack mem. Heap mem.
m count
m obj*
ctrl blk
ctrl blk*
shared ptr
7 / 26
Smart Pointers std::shared ptr
1 void rc(const std::shared_ptr<int> &x) {
2 std::cout << x.use_count();
3 }
4
5 void foo(std::shared_ptr<int> arg) {
6 rc();
7 auto z = arg;
8 rc();
9 }
10
11 int main() {
12 auto x = std::make_shared<int>(100);
13 rc(x);
14 {
15 auto a = x; rc(x);
16 auto b = x; rc(x);
17 auto c = x; rc(x);
18 foo(x); rc(x);
19 }
20 rc(x);
21 }
Link to godbolt
Stack mem. Heap mem.
m count
m obj*
ctrl blk
ctrl blk*
shared ptr
8 / 26
Thread Safety
Stack mem. Heap mem.
m count
Yes, Atomic.
m obj*
ctrl blk
m obj
Not necessarily!
ctrl blk*
shared ptr
9 / 26
Thread Safety
Stack mem. Heap mem.
m count
Yes, Atomic.
m obj*
ctrl blk
m obj
Not necessarily!
ctrl blk*
shared ptr
9 / 26
Thread Safety
Stack mem. Heap mem.
m count
Yes, Atomic.
m obj*
ctrl blk
m obj
Not necessarily!
ctrl blk*
shared ptr
9 / 26
Thread Safety
Stack mem. Heap mem.
m count
Yes, Atomic.
m obj*
ctrl blk
m obj
Not necessarily!
ctrl blk*
shared ptr
9 / 26
Problem 1. Data Race on Object
1 std::shared_ptr<int> ptr =
2 std::make_shared<int>(0);
3
4 auto reader = std::thread(
5 [](std::shared_ptr<int> ptr) {
6 for(int i = 0; i < 100; i++)
7 printf("%d ", *ptr);
8 printf("\n");
9 }, ptr);
10
11 auto writer = std::thread(
12 [](std::shared_ptr<int> ptr) {
13 for(int i = 0; i < 100; i++)
14 *ptr = i;
15 }, ptr);
16
17 reader.join();
18 writer.join();
Link to godbolt
The underlying
object is not thread
safe!
Extra: How to fix?
10 / 26
Problem 1. Data Race on Object
1 std::shared_ptr<int> ptr =
2 std::make_shared<int>(0);
3
4 auto reader = std::thread(
5 [](std::shared_ptr<int> ptr) {
6 for(int i = 0; i < 100; i++)
7 printf("%d ", *ptr);
8 printf("\n");
9 }, ptr);
10
11 auto writer = std::thread(
12 [](std::shared_ptr<int> ptr) {
13 for(int i = 0; i < 100; i++)
14 *ptr = i;
15 }, ptr);
16
17 reader.join();
18 writer.join();
Link to godbolt
The underlying
object is not thread
safe!
Extra: How to fix?
10 / 26
Problem 1: Data Race on Object
1 std::shared_ptr<int> ptr =
2 std::make_shared<int>(0);
3
4 auto reader = std::thread(
5 [](std::shared_ptr<int> ptr) {
6 for(int i = 0; i < 100; i++)
7 printf("%d ", *ptr);
8 printf("\n");
9 }, ptr);
10
11 auto writer = std::thread(
12 [](std::shared_ptr<int> ptr) {
13 for(int i = 0; i < 100; i++)
14 *ptr = i;
15 }, ptr);
16
17 reader.join();
18 writer.join();
Link to godbolt
Stack mem. (main)
Stack mem. (reader)
Stack mem. (writer)
Heap mem.
m count
m obj*
ctrl blk
ctrl blk*
shared ptr
11 / 26
Problem 2. Data Race on Pointer
1 std::shared_ptr<int> ptr;
2
3 auto reader = std::thread(
4 [](std::shared_ptr<int>& ptr) {
5 while(ptr == nullptr);
6
printf("%d\n", *ptr);
7 }, std::ref(ptr));
8
9 auto writer = std::thread(
10 [](std::shared_ptr<int>& ptr) {
11 for(int i = 0; i < 100; i++)
12 ptr = std::make_shared<int>(i);
13 }, std::ref(ptr));
14
15 reader.join();
16 writer.join();
Link to godbolt
Data race on ptr
itself!
12 / 26
Problem 2. Data Race on Pointer
1 std::shared_ptr<int> ptr;
2
3 auto reader = std::thread(
4 [](std::shared_ptr<int>& ptr) {
5 while(ptr == nullptr);
6
printf("%d\n", *ptr);
7 }, std::ref(ptr));
8
9 auto writer = std::thread(
10 [](std::shared_ptr<int>& ptr) {
11 for(int i = 0; i < 100; i++)
12 ptr = std::make_shared<int>(i);
13 }, std::ref(ptr));
14
15 reader.join();
16 writer.join();
Link to godbolt
Data race on ptr
itself!
12 / 26
Problem 2: Data Race on Pointer
1 std::shared_ptr<int> ptr;
2
3 auto reader = std::thread(
4 [](std::shared_ptr<int>& ptr) {
5 while(ptr == nullptr);
6
printf("%d\n", *ptr);
7 }, std::ref(ptr));
8
9 auto writer = std::thread(
10 [](std::shared_ptr<int>& ptr) {
11 for(int i = 0; i < 100; i++)
12 ptr = std::make_shared<int>(i);
13 }, std::ref(ptr));
14
15 reader.join();
16 writer.join();
Link to godbolt
Stack mem. (main)
Stack mem. (reader)
Stack mem. (writer)
Heap mem.
m count
m obj*
ctrl blk
m count
m obj*
ctrl blk
ctrl blk*
shared ptr
13 / 26
Problem 3. Circular Reference
1 struct DLLNode
2 {
3 std::shared_ptr<DLLNode> prev;
4 std::shared_ptr<DLLNode> next;
5 };
6
7 struct DLL
8 {
9 std::shared_ptr<DLLNode> head {};
10 std::shared_ptr<DLLNode> tail {};
11
12 void push_front(std::shared_ptr<DLLNode>);
13 void push_back(std::shared_ptr<DLLNode>);
14
15 std::shared_ptr<DLLNode> front();
16 std::shared_ptr<DLLNode> back();
17 };
18
Link to godbolt
Run a->next = b
and b->prev = a.
Extra: How to fix?
14 / 26
Problem 3. Circular Reference
1 struct DLLNode
2 {
3 std::shared_ptr<DLLNode> prev;
4 std::shared_ptr<DLLNode> next;
5 };
6
7 struct DLL
8 {
9 std::shared_ptr<DLLNode> head {};
10 std::shared_ptr<DLLNode> tail {};
11
12 void push_front(std::shared_ptr<DLLNode>);
13 void push_back(std::shared_ptr<DLLNode>);
14
15 std::shared_ptr<DLLNode> front();
16 std::shared_ptr<DLLNode> back();
17 };
18
Link to godbolt
Run a->next = b
and b->prev = a.
Extra: How to fix?
14 / 26
Problem 3. Circular Reference
Stack mem. Heap mem.
m count
m obj*
ctrl blk
m count
m obj*
ctrl blk
ctrl blk*
head
ctrl blk*
tail
DLLNode
prev
ctrl blk*
next
ctrl blk*
DLLNode
prev
ctrl blk*
next
ctrl blk*
15 / 26
Contents
Why Smart Pointers?
std::unique ptr
std::shared ptr
Common Pitfalls when using std::shared ptr
Implementing Shared Pointer
16 / 26
Attempt 1: Using a Mutex
1 template <typename T>
2 struct SharedPtr {
3 private:
4 size_t m_count; T* m_ptr;
5
6 public:
7 SharedPtr(T* ptr) : m_count(1), m_ptr(ptr) {}
8
9 SharedPtr(const SharedPtr& other)
10 : m_count(other.m_count) , m_ptr(other.m_ptr) {
11 ++m_count;
12 }
13
14 ~SharedPtr() {
15 if((--m_count) == 0)
16 delete m_ptr;
17 }
18
19 T* get() { return m_ptr; }
20 const T* get() const { return m_ptr; }
21 };
Link to godbolt
Use-after-free
Situation!
We must not
unlock the mutex
after we free it.
17 / 26
Attempt 1: Using a Mutex
1 template <typename T>
2 struct SharedPtr {
3 private:
4 size_t m_count; T* m_ptr;
5
6 public:
7 SharedPtr(T* ptr) : m_count(1), m_ptr(ptr) {}
8
9 SharedPtr(const SharedPtr& other)
10 : m_count(other.m_count) , m_ptr(other.m_ptr) {
11 ++m_count;
12 }
13
14 ~SharedPtr() {
15 if((--m_count) == 0)
16 delete m_ptr;
17 }
18
19 T* get() { return m_ptr; }
20 const T* get() const { return m_ptr; }
21 };
Link to godbolt
Use-after-free
Situation!
We must not
unlock the mutex
after we free it.
17 / 26
Attempt 2: Atomic Reference Counter
1 template <typename T>
2 struct SharedPtr {
3 private:
4 size_t m_count; T* m_ptr;
5
6 public:
7 SharedPtr(T* ptr) : m_count(1), m_ptr(ptr) {}
8
9 SharedPtr(const SharedPtr& other)
10 : m_count(other.m_count) , m_ptr(other.m_ptr) {
11 ++m_count;
12 }
13
14 ~SharedPtr() {
15 if((--m_count) == 0)
16 delete m_ptr;
17 }
18
19 T* get() { return m_ptr; }
20 const T* get() const { return m_ptr; }
21 };
Link to godbolt
Can we weaken the
memory order?
18 / 26
Attempt 2: Atomic Reference Counter
1 template <typename T>
2 struct SharedPtr {
3 private:
4 size_t m_count; T* m_ptr;
5
6 public:
7 SharedPtr(T* ptr) : m_count(1), m_ptr(ptr) {}
8
9 SharedPtr(const SharedPtr& other)
10 : m_count(other.m_count) , m_ptr(other.m_ptr) {
11 ++m_count;
12 }
13
14 ~SharedPtr() {
15 if((--m_count) == 0)
16 delete m_ptr;
17 }
18
19 T* get() { return m_ptr; }
20 const T* get() const { return m_ptr; }
21 };
Link to godbolt
Can we weaken the
memory order?
18 / 26
Attempt 3: Relaxed Reference Counting
Is there a data race?
1 SharedPtr(const SharedPtr& other)
2 : m_count(other.m_count)
3 , m_ptr(other.m_ptr) {
4 m_count->fetch_add(1,
5 std::memory_order::relaxed);
6 }
1 ~SharedPtr() {
2 size_t old_count =
3 m_count->fetch_sub(1,
4 std::memory_order::relaxed);
5 if (old_count == 1) {
6 delete m_ptr;
7 delete m_count;
8 }
9 }
19 / 26
Attempt 3: Relaxed Refernece Counting
Last Thread
count--
delete count
sb
(1 0)
2nd Last Thread
count++
spawn thread
count--
(2 1)
sb
sb
sw
20 / 26
Attempt 3: Relaxed Refernece Counting
Last Thread
count--
delete count
sb
(1 0)
2nd Last Thread
count++
spawn thread
count--
(2 1)
sb
sb
sw
20 / 26
Attempt 3: Relaxed Refernece Counting
Last Thread
count--
delete count
sb
(1 0)
2nd Last Thread
count++
spawn thread
count--
(2 1)
sb
sb
sw
20 / 26
Attempt 3: Relaxed Refernece Counting
Last Thread
count--
delete count
sb
(1 0)
2nd Last Thread
count++
spawn thread
count--
(2 1)
sb
sb
sw
20 / 26
What happens if we have 3 threads?
Last Thread
count--
delete count
sb
(1 0)
2nd Last Thread
count++
spawn thread
count--
(2 1)
sb
sb
sw
Random Thread
count++
spawn thread
count--
(? ?)
sb
sb
sw?
21 / 26
What happens if we have 3 threads?
Last Thread
count--
delete count
sb
(1 0)
2nd Last Thread
count++
spawn thread
count--
(2 1)
sb
sb
sw
Random Thread
count++
spawn thread
count--
(? ?)
sb
sb
sw?
21 / 26
What happens if we have 3 threads?
Last Thread
count--
delete count
sb
(1 0)
2nd Last Thread
count++
spawn thread
count--
(2 1)
sb
sb
sw
Random Thread
count++
spawn thread
count--
(? ?)
sb
sb
sw?
21 / 26
Release Sequences
Release Sequence: Maximal contiguous subsequence of atomic read-modify-write
(RMW) operations that follow in the modification order of the memory location.
Guarantee: An atomic release A synchronizes with an atomic acquire B that
reads from the release sequence headed by A.
A B C D E
modication order modication order modication order modification order
fetch add fetch sub fetch add
store
fetch sub
acq rel
relaxed
seq cst
release
acq rel
22 / 26
Release Sequences in Action
Last Thread
count--
delete count
sb
(1 0)
2nd Last Thread
count++
spawn thread
count--
(2 1)
sb
sb
sw
Random Thread
count++
spawn thread
count--
(? ?)
sb
sb
23 / 26
Summary
std::unique ptr: Turns non-RAII types into RAII types. Enforces unique
ownership.
std::shared ptr: Performs reference counting to support shared ownership.
We implemented std::shared ptr using mutex and atomics!
Mutex: Avoid use-after-free situation.
Atomics: Release sequences.
24 / 26
Attendance
Session ID: 1939881
CS3211 Tutorial 3
12 February 2026 14:00-16:00
BIZ2-02-02 - SEMINAR ROOM 2-2
25 / 26
That’s it!
Anonymous Feedback (throughout the semester):
https://forms.gle/6a9T4t88wNwYxG4G8
The link to the tutorial slides will be posted in telegram.
26 / 26