Introduction

Smart pointers are mainly used to automate the management of resources. There are std::unqiue_ptr, std::shared_ptr and std::weak_ptr according to their usage.

unique_ptr

unique_ptr is mainly used to implement exclusive management of a resource object whose life cycle will end at the end of the unique_ptr declaration cycle or when it is pointed to another resource. A resource managed by unique_ptr can be transferred to another unqiue_ptr via std::move.

The following are some simple examples.

 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
31
32
33
34
35
36
37
38
#include <iostream>
#include <memory>

class Base {
public:
    Base(int id) : id(id), count(new int(1000)) { std::cout << "Base::Base() for " << id << std::endl; }

    ~Base() {
        delete count;
        std::cout << "Base::~Base() for " << id << std::endl;
    }

private:
    int id = 0;
    int* count = nullptr;
};

int main() {
    // unqiue_ptr would dispose resources when going out of scope
    std::cout << "-------------\n";
    { std::unique_ptr<Base> p2 = std::make_unique<Base>(1); }
    std::cout << "-------------\n";

    // unqiue_ptr can pass resource to another smart pointers
    std::cout << "-------------\n";
    {
        std::unique_ptr<Base> p2 = nullptr;
        std::cout << "================\n";
        {
            std::unique_ptr<Base> p3 = std::make_unique<Base>(2);
            p2 = std::move(p3);
        }
        std::cout << "==================\n";
    }
    std::cout << "-------------\n";

    return 0;
}

The results of the run are.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
-------------
Base::Base() for 1
Base::~Base() for 1
-------------
-------------
================
Base::Base() for 2
==================
Base::~Base() for 2
-------------

shared_ptr

In some cases, we need to share ownership of a resource (operation rights), such as a graph structure of nodes and edges, each edge may be two nodes, and each node can be connected to multiple edges, so if we have an edge structure, then each edge involving a node needs to have ownership of that node. At this point unqiue_ptr does not meet the need, so we need to introduce shared_ptr to achieve common management of nodes. shared_ptr maintains an internal reference count, and the resource object is destructured only when all objects of shared_ptr have exceeded their lifecycle (the reference count drops to 0), as follows.

 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
31
32
33
34
35
#include <iostream>
#include <memory>

class Base {
public:
    Base(int id) : id(id), count(new int(1000)) { std::cout << "Base::Base() for " << id << std::endl; }

    ~Base() {
        delete count;
        std::cout << "Base::~Base() for " << id << std::endl;
    }

private:
    int id = 0;
    int* count = nullptr;
};

int main() {
    // shared_ptr can be used to shared management for resource
    std::cout << "-------------\n";
    {
        std::shared_ptr<Base> p1 = std::make_shared<Base>(1);
        std::cout << "Use count for 1: " << p1.use_count() << std::endl;
        std::cout << "================\n";
        {
            std::shared_ptr<Base> p2 = p1;
            std::cout << "Use count for 1: " << p1.use_count() << std::endl;
        }
        std::cout << "================\n";
        std::cout << "Use count for 1: " << p1.use_count() << std::endl;
    }
    std::cout << "-------------\n";

    return 0;
}

The results of the run are shown below.

1
2
3
4
5
6
7
8
9
-------------
Base::Base() for 1
Use count for 1: 1
================
Use count for 1: 2
================
Use count for 1: 1
Base::~Base() for 1
-------------

weak_ptr

Sometimes the misuse of shared_ptr may cause circular reference problems. An example is the problem with nodes and edges in graphs mentioned in the previous section. Suppose we have a node class Node and an edge class Edge. Suppose we let Node use shared_ptr to point to an Edge object, and let that object use shared_ptr to point to a Node object. That is, a pair of Node and Edge objects contain a shared_ptr pointing to each other, at this time, if the node object to release, you must first let the edge object destruct release trigger the smart pointer to release the node, the same for the edge object, so that the reference loop occurs, after the two objects out of their scope, they point to the object still exists in memory in memory and cannot be fetched again, which can lead to memory leaks as follows.

 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
31
32
33
34
35
36
37
38
39
40
41
#include <iostream>
#include <memory>

class Node;

class Edge {
public:
    Edge() { std::cout << "Edge Constructed.\n"; }
    ~Edge() { std::cout << "Edge Destroyed.\n"; }

    void setNode(std::shared_ptr<Node>& node) { node_ = node; }

private:
    std::shared_ptr<Node> node_;
};

class Node {
public:
    Node() { std::cout << "Node Constructed.\n"; }
    ~Node() { std::cout << "Edge Destroyed.\n"; }

    void setEdge(std::shared_ptr<Edge>& edge) { edge_ = edge; }

private:
    std::shared_ptr<Edge> edge_;
};

int main() {
    // shared_ptr can be used to shared management for resource
    std::cout << "-------------\n";
    {
        std::shared_ptr<Edge> edge = std::make_shared<Edge>();
        std::shared_ptr<Node> node = std::make_shared<Node>();

        edge->setNode(node);
        node->setEdge(edge);
    }
    std::cout << "-------------\n";

    return 0;
}

The results of the run are shown below.

1
2
3
4
-------------
Edge Constructed.
Node Constructed.
-------------

To solve this problem, there are several ideas.

  • First think about whether it is necessary to include other classes in all classes, for example, under this example, is it necessary to know the information corresponding to it as a node? If it is not necessary, we can just remove the edge variable from the node
  • If we need to keep information about each other in both nodes and edges in order to perform certain operations, a better approach might be to create a new graph class Graph to unify the management of edges and nodes, and keep only the ids of the related variables in Node and Edge, which can be retrieved from the graph when needed.
  • If we don’t want to manage these variables externally to reduce function calls or for other reasons, then we can use weak_ptr to reduce the privileges of one variable over another.

weak_ptr can be used as a weak reference to an object managed by shared_ptr, equivalent to a companion set of shared_ptr. In terms of usage, weak_ptr cannot operate on resources directly, but needs to be checked for legality (existence) by expired() before use, and must be temporarily converted to shared_ptr before it can be used. This ensures that the lifecycle of shared_ptr is not too long. In the above example, we can change the edge in the node class to weak_ptr.

 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
31
32
33
34
35
36
37
38
39
40
41
#include <iostream>
#include <memory>

class Node;

class Edge {
public:
    Edge() { std::cout << "Edge Constructed.\n"; }
    ~Edge() { std::cout << "Edge Destroyed.\n"; }

    void setNode(std::shared_ptr<Node>& node) { node_ = node; }

private:
    std::weak_ptr<Node> node_;
};

class Node {
public:
    Node() { std::cout << "Node Constructed.\n"; }
    ~Node() { std::cout << "Node Destroyed.\n"; }

    void setEdge(std::shared_ptr<Edge>& edge) { edge_ = edge; }

private:
    std::shared_ptr<Edge> edge_;
};

int main() {
    // shared_ptr can be used to shared management for resource
    std::cout << "-------------\n";
    {
        std::shared_ptr<Edge> edge = std::make_shared<Edge>();
        std::shared_ptr<Node> node = std::make_shared<Node>();

        edge->setNode(node);
        node->setEdge(edge);
    }
    std::cout << "-------------\n";

    return 0;
}
1
2
3
4
5
6
-------------
Edge Constructed.
Node Constructed.
Edge Destroyed.
Edge Destroyed.
-------------

A simple implementation of smart pointers

After understanding the principle of smart pointers, we can perform a simple implementation of smart pointers.

Unique_ptr

The point of Unique_ptr is to restrict the copy construction and assignment of resource resources, and to ensure that the move construction and assignment holds, as follows.

 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
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
template <typename T>
class Unique_ptr {
public:
    /******************* Construct and Assign ********************/
    Unique_ptr(T* res = nullptr) : res_(res) {}
    Unique_ptr& operator=(T* res) {
        delete res_;
        res_ = res;
        return *this;
    }

    Unique_ptr(const Unique_ptr& rhs) = delete;
    Unique_ptr& operator=(const Unique_ptr& rhs) = delete;

    Unique_ptr(Unique_ptr&& rhs) {
        if (rhs.res_ == res_) return;  // do no change
        if (res_) delete res_;

        res_ = rhs.res_;
        rhs.res_ = nullptr;  // make sure that make rhs own nullptr
    }
    Unique_ptr& operator=(Unique_ptr&& rhs) {
        if (rhs.res_ == res_) return *this;  // do no change
        if (res_) delete res_;

        res_ = rhs.res_;
        rhs.res_ = nullptr;  // make sure that make rhs own nullptr

        return *this;
    }
    /******************* Construct and Assign ********************/

    /******************* Usage ************************/
    explicit operator bool() const { return res_; }
    T* operator*() const { return res_; }
    T& operator->() const { return *res_; }
    /******************* Usage ************************/

    ~Unique_ptr() {
        if (res_) delete res_;
    }

private:
    T* res_ = nullptr;
};

Shared_ptr

The point of Shared_ptr is to maintain a reference technique for resources, update the reference count when reassigning or destructing, and release resources when the reference count is 0.

 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
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
template <typename T>
class Shared_ptr {
public:
    /******************* Construct and Assign ********************/
    Shared_ptr(T* res = nullptr) : res_(res), count_(new int) {
        if (res_) *count_ = 1;
    }
    Shared_ptr& operator=(T* res) {
        if (res == res_) return *this;

        (*count_)--;
        if (*count_ == 0) delete res_;

        res_ = res;
        if (res_) *count_ = 1;
        return *this;
    }

    Shared_ptr(const Shared_ptr& rhs) : res_(rhs.res_), count_(rhs.count_) { (*count_)++; }
    Shared_ptr& operator=(const Shared_ptr& rhs) {
        if (rhs.res_ == res_) return *this;

        (*count_)--;
        if (*count_ == 0) delete res_;

        res_ = rhs.res;
        count_ = rhs.count_;
        *count_++;

        return *this;
    }
    /******************* Construct and Assign ********************/

    /******************* Usage ************************/
    explicit operator bool() const { return res_; }
    T* operator*() const { return res_; }
    T& operator->() const { return *res_; }
    int use_count() const { return *count_; }
    /******************* Usage ************************/

    ~Shared_ptr() {
        (*count_)--;
        if (*count_ == 0) {
            if (res_) delete res_;
            delete count_;
        }
    }

private:
    T* res_ = nullptr;
    int* count_ = nullptr;
};