C++

C++ - [Pointers - 2] - Smart Pointers

unique_ptr, shared_ptr

Posted by Rico's Nerd Cluster on March 18, 2023

Unique Pointer

Basics

1
2
#include <memory>
std::unique_ptr<int> ptr = std::make_unique<int>(1);
  • Note that a unique_ptr is only 8 bytes on a 64 bit system, and it’s the same size as a raw pointer, because it is just a wrapper that ensures sole ownership of the pointer.

Shared Pointer

Memory Usage of Shared Pointer

Note that a shared_ptr is 2x8=16 bytes itself. It includes: - a raw pointer - a pointer to the control block.

The control block has a reference counter, deleter, etc. Its implementation is platform-dependent. Roughly, we need 8 bytes for the count, and 8 bytes for the pointer to the deleter. So, 16 bytes.

Can you see the difference here?

1
2
3
auto ptr = std::make_shared<Type> (args);
// vs
auto ptr2 = std::shared_pointer<Type> (new Type());
  • ptr has 1 memory allocation call: control block + the object itself
  • ptr2 has 2 memory allocation calls: control block and the object itself separately

Weak Pointer

Usage: when two pointers could point to each other (cyclic reference) . Wrong example:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
class Student {
    shared_ptr<Student> bestFriend;
public:
    void makeFriend(shared_ptr<Student> other) {
        bestFriend = other;
    }
};

auto tom = make_shared<Student>();
auto jerry = make_shared<Student>();
tom->makeFriend(jerry);    // tom拉住jerry
jerry->makeFriend(tom);    // jerry也拉住tom

// Cyclic memory free!
  • shared_ptr increments reference counts of each other’s members. So this will increase each student’s count. So the rule of thumb is use the weak pointer in a class which stores a pointer to another instance of the same class!

Correct version:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
#include <memory>

class Student {
    std::weak_ptr<Student> bestFriend;  // non-owning reference
public:
    void makeFriend(const std::shared_ptr<Student>& other) {
        bestFriend = other;
    }

    std::shared_ptr<Student> getBestFriend() const {
        return bestFriend.lock(); // may be nullptr if expired
    }
};

int main() {
    auto tom   = std::make_shared<Student>();
    auto jerry = std::make_shared<Student>();

    tom->makeFriend(jerry);
    jerry->makeFriend(tom);

    // No cycle of ownership: objects are freed normally.
}


Common Operations

Reset: std::unique_ptr::reset() and std::shared_ptr::reset

  • unique_ptr::reset() is basically basically delete ptr; ptr = nullptr
  • std::shared_ptr::reset() can be a bit slower, because of deallocation & allocation of referecen count.
1
2
3
4
5
std::unique_ptr<int> ptr;
ptr.reset(new int(1));
std::shared_ptr<int> s_ptr;
s_ptr = std::make_shared<int>(3);
s_ptr.reset(new int(3));

Ownership:

  • unique_ptr is move-assigned.
1
2
3
4
5
6
// ownership
// unique_ptr is move-assigned
std::unique_ptr<int> new_ptr = std::make_unique<int>(5);
// If the count reaches 0, it also deletes the object and the control block
std::shared_ptr<int> s_ptr2 = std::make_shared<int>(6);
s_ptr = s_ptr2; // now the element 3 is deleted since it's not referenced

Conversions

unique_ptr -> shared_ptr: use std::move to transfer ownership

1
2
3
4
5
std::unique_ptr<Foo> u = std::make_unique<Foo>();

std::shared_ptr<Foo> s = std::move(u);    // OK
// or:
std::shared_ptr<Foo> s2(std::move(u));    // OK

share_from_this

When you want to pass this as a pointer to another object, you must use share_from_this()

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
#include <iostream>
#include <memory>
#include <thread>
#include <chrono>
#include <functional>

struct Worker : std::enable_shared_from_this<Worker> {
    void startAsyncJob() {
        auto self = shared_from_this();
        std::thread([self] {
            self->onJobDone();
            std::this_thread::sleep_for(std::chrono::milliseconds(2000));
        }).detach();
    }
    void onJobDone() {   
        std::cout << "Job done, Worker is still alive.\n";
    }

    ~Worker() {
        std::cout << "Worker destroyed.\n";
    }
};


int main() {
    auto w = std::make_shared<Worker>();
    w->startAsyncJob();
    w.reset();
    std::this_thread::sleep_for(std::chrono::milliseconds(5000));
}
  • Note, in this example, w.reset() would NOT call the destructor because the detached thread now owns Worker. So, Worker destroyed is called when the worker function is done.

Caveat: shared_from_this cannot be used in constructor, because shared_ptr won’t be ready yet

Instead, use a reference to the object

1
2
3
Foo::Foo() {
  Foo& r = *this;   // always valid
}