std::unique_ptr

unique_ptr It is a pointer to exclusive resource ownership. unique_ptr is allocated on the stack and then freed after leaving the scope, deleting the Resource object held inside.

As of C++ 11, we can use unique_ptr like this.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
#include <iostream>
#include <memory> // for std::unique_ptr

class Resource
{
public:
    Resource() { std::cout << "Resource acquired\n"; }
    ~Resource() { std::cout << "Resource destroyed\n"; }
};

int main()
{
    // allocate a Resource object and have it owned by std::unique_ptr
    std::unique_ptr<Resource> res{ new Resource() };

    return 0;
} // res goes out of scope here, and the allocated Resource is destroyed

New in C++14 is the make_unique function, which allows us to construct a unique_ptr object (supporting array objects).

 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 <memory> // for std::unique_ptr and std::make_unique
#include <iostream>

class Fraction
{
private:
    int m_numerator{ 0 };
    int m_denominator{ 1 };

public:
    Fraction(int numerator = 0, int denominator = 1) :
        m_numerator{ numerator }, m_denominator{ denominator }
    {
    }

    friend std::ostream& operator<<(std::ostream& out, const Fraction &f1)
    {
        out << f1.m_numerator << '/' << f1.m_denominator;
        return out;
    }
};

int main()
{
    // Create a single dynamically allocated Fraction with numerator 3 and denominator 5
    // We can also use automatic type deduction to good effect here
    auto f1{ std::make_unique<Fraction>(3, 5) };
    std::cout << *f1 << '\n';

    // Create a dynamically allocated array of Fractions of length 4
    auto f2{ std::make_unique<Fraction[]>(4) };
    std::cout << f2[0] << '\n';

    return 0;
}

The output is as follows.

1
2
3/5
0/1

operation unique_ptr

unique_ptr removes the copy constructor and copy assignment operator, so it uses the move semantics when assigning values, and must use move to transfer objects when transferring them.

 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
#include <iostream>
#include <memory> // for std::unique_ptr
#include <utility> // for std::move

class Resource
{
public:
    Resource() { std::cout << "Resource acquired\n"; }
    ~Resource() { std::cout << "Resource destroyed\n"; }
};

int main()
{
    std::unique_ptr<Resource> res1{ new Resource{} }; // Resource created here
    std::unique_ptr<Resource> res2{}; // Start as nullptr

    std::cout << "res1 is " << (res1 ? "not null\n" : "null\n");
    std::cout << "res2 is " << (res2 ? "not null\n" : "null\n");

    // res2 = res1; // Won't compile: copy assignment is disabled
    res2 = std::move(res1); // res2 assumes ownership, res1 is set to null

    std::cout << "Ownership transferred\n";

    std::cout << "res1 is " << (res1 ? "not null\n" : "null\n");
    std::cout << "res2 is " << (res2 ? "not null\n" : "null\n");

    return 0;
} // Resource destroyed here when res2 goes out of scope

The output is as follows.

1
2
3
4
5
6
7
Resource acquired
res1 is not null
res2 is null
Ownership transferred
res1 is null
res2 is not null
Resource destroyed

unique_ptr also overloads operator* and operator->, where operator* returns a reference to an object and operator-> returns a pointer to an object. Note that the objects managed by unique_ptr can be transferred, so you can determine this by if before using these two overloaded methods.

 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 <iostream>
#include <memory> // for std::unique_ptr

class Resource
{
public:
    Resource() { std::cout << "Resource acquired\n"; }
    ~Resource() { std::cout << "Resource destroyed\n"; }
    friend std::ostream& operator<<(std::ostream& out, const Resource &res)
    {
        out << "I am a resource";
        return out;
    }
};

int main()
{
    std::unique_ptr<Resource> res{ new Resource{} };

    if (res) // use implicit cast to bool to ensure res contains a Resource
        std::cout << *res << '\n'; // print the Resource that res is owning

    return 0;
}

The output is as follows.

1
2
3
Resource acquired
I am a resource
Resource destroyed

Normally, we don’t have to worry about releasing objects managed by unique_ptr after we use it. But sometimes we still want to get back ownership of the object, so we can use the release function to return the pointer to the object managed by unique_ptr and release control of the pointer.

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

int main() { 
    std::unique_ptr<int> uptr(new int(10));  //Binding dynamic objects
    std::unique_ptr<int> uptr2 = std::move(uptr); //Transfer of Ownership

    if(uptr == nullptr)
        cout<<"uptr give up *int"<<endl; 

    int * p = uptr2.release(); //uptr2 releases control of the pointer, returns the pointer, and sets uptr2 to null

    if(uptr2 == nullptr)
        cout<<"uptr2 give up *int"<<endl; 

    cout<< *p <<endl; 
    delete p; 
    return 0;
}

unique_ptr as an argument and return value

unique_ptr passed as an argument will not be copied, but will transfer ownership of the object to the function, as follows ptr will be destroyed before the main method ends.

 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
#include <iostream>
#include <memory> // for std::unique_ptr
#include <utility> // for std::move

class Resource
{
public:
    Resource() { std::cout << "Resource acquired\n"; }
    ~Resource() { std::cout << "Resource destroyed\n"; }
    friend std::ostream& operator<<(std::ostream& out, const Resource &res)
    {
        out << "I am a resource";
        return out;
    }
};

void takeOwnership(std::unique_ptr<Resource> res)
{
     if (res)
          std::cout << *res << '\n';
} // the Resource is destroyed here

int main()
{
    auto ptr{ std::make_unique<Resource>() };

//    takeOwnership(ptr); // This doesn't work, need to use move semantics
    takeOwnership(std::move(ptr)); // ok: use move semantics

    std::cout << "Ending program\n";

    return 0;
}

The output is as follows.

1
2
3
4
Resource acquired
I am a resource
Resource destroyed
Ending program

Sometimes you don’t want to transfer the ownership of the object to the function, then you can get the object through the get method, 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 <memory> // for std::unique_ptr
#include <iostream>

class Resource
{
public:
    Resource() { std::cout << "Resource acquired\n"; }
    ~Resource() { std::cout << "Resource destroyed\n"; }

    friend std::ostream& operator<<(std::ostream& out, const Resource &res)
    {
        out << "I am a resource";
        return out;
    }
};

// The function only uses the resource, so we'll accept a pointer to the resource, not a reference to the whole std::unique_ptr<Resource>
void useResource(Resource* res)
{
    if (res)
        std::cout << *res << '\n';
    else
        std::cout << "No resource\n";
}

int main()
{
    auto ptr{ std::make_unique<Resource>() };

    useResource(ptr.get()); // note: get() used here to get a pointer to the Resource

    std::cout << "Ending program\n";

    return 0;
} // The Resource is destroyed here

The output is as follows.

1
2
3
4
Resource acquired
I am a resource
Ending program
Resource destroyed

unique_ptr can be returned directly as a return value.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
#include <memory> // for std::unique_ptr

std::unique_ptr<Resource> createResource()
{
     return std::make_unique<Resource>();
}

int main()
{
    auto ptr{ createResource() };

    // do whatever

    return 0;
}

In C++14 and earlier versions would use the move semantics to return unique_ptr objects, in C++17 and later versions, the move operation is also not needed because the return value optimization is forced on, and RVO optimization is performed (see here for details: https://en.wikipedia.org/wiki/Copy_elision), so it is safer to return the value directly than to return a raw pointer or reference.

Caution

Do not let multiple unique_ptrs hold the same object. As follows, res1 and res2 will try to release res multiple times.

1
2
3
Resource* res{ new Resource() };
std::unique_ptr<Resource> res1{ res };
std::unique_ptr<Resource> res2{ res };

Do not delete an object manually after holding it with unique_ptr, as this will cause an object to be freed multiple times.

1
2
3
Resource* res{ new Resource() };
std::unique_ptr<Resource> res1{ res };
delete res;

Note also the exception issue, using unique_ptr does not absolutely guarantee exception safety, as follows.

1
some_function(std::unique_ptr<T>(new T), function_that_can_throw_exception());

The C++ standard does not specify the order in which the compiler evaluates function arguments, so it is possible to have an order such as

  • calls to new T to allocate dynamic memory.
  • calling the function_that_can_throw_exception() function.
  • Calling the constructor of unique_ptr.

If function_that_can_throw_exception is called and unique_ptr is not constructed at that time, the memory allocated by new T cannot be reclaimed, resulting in a memory leak. To solve this problem, you need to use the make_unique function.

1
some_function(std::make_unique<T>(), function_that_can_throw_exception());

Because the creation of object T and unique_ptr are handled in the make_unique function, the above order problem does not occur.

std::shared_ptr

shared_ptr is different from unique_ptr in that the resources it manages can be held by multiple objects. In the underlying implementation, shared_ptr is implemented using reference counting, which has an internal controller type hosting a counter, and this internal controller type object is stored in shared_ptr as a pointer when shared_ptr is first constructed.

shared_ptr overloads the assignment operator, and this pointer is shared by another shared_ptr when it is assigned and copied to construct another shared_ptr. When the reference count goes to zero, this internally typed pointer is released along with the resources managed by shared_ptr.

In addition, to ensure thread safety, the reference counter’s plus 1 and minus 1 operations are atomic operations, which ensure that no problems occur when the shared_ptr is shared by multiple threads.

For simple use see the following example.

 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
#include <iostream>
#include <memory> // for std::shared_ptr

class Resource
{
public:
    Resource() { std::cout << "Resource acquired\n"; }
    ~Resource() { std::cout << "Resource destroyed\n"; }
};

int main()
{
    // allocate a Resource object and have it owned by std::shared_ptr
    Resource* res { new Resource };
    std::shared_ptr<Resource> ptr1{ res };
    {
        std::shared_ptr<Resource> ptr2 { ptr1 }; // make another std::shared_ptr pointing to the same thing

        std::cout << "Killing one shared pointer\n";
    } // ptr2 goes out of scope here, but nothing happens

    std::cout << "Killing another shared pointer\n";

    return 0;
} // ptr1 goes out of scope here, and the allocated Resource is destroyed

The output is as follows.

1
2
3
4
Resource acquired
Killing one shared pointer
Killing another shared pointer
Resource destroyed

In the above example, ptr2 is created in its own scope, and then out of scope ptr2 is destroyed, but the managed resources are not destroyed until after the main method is finished.

Here’s another example, which is one of the wrong uses for many people.

 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 <iostream>
#include <memory> // for std::shared_ptr

class Resource
{
public:
    Resource() { std::cout << "Resource acquired\n"; }
    ~Resource() { std::cout << "Resource destroyed\n"; }
};

int main()
{
    Resource* res { new Resource };
    std::shared_ptr<Resource> ptr1 { res };
    {
        std::shared_ptr<Resource> ptr2 { res }; // create ptr2 directly from res (instead of ptr1)

        std::cout << "Killing one shared pointer\n";
    } // ptr2 goes out of scope here, and the allocated Resource is destroyed

    std::cout << "Killing another shared pointer\n";

    return 0;
} // ptr1 goes out of scope here, and the allocated Resource is destroyed again

The output is as follows.

1
2
3
4
5
Resource acquired
Killing one shared pointer
Resource destroyed
Killing another shared pointer
Resource destroyed

The above example will cause a crash because the same resource is deleted twice. We notice that ptr2 is not copied from ptr1, but created directly. Even though the resources managed internally are the same, they are not related to each other, so when ptr2 goes out of scope, it will be freed, and ptr1 will be freed again after the main method ends.

So if you need a shared_ptr to point to the same resource, then best practice is to copy it out of an existing shared_ptr.

Also new in C++14 is the make_shared function, which we can use to construct a shared_ptr object.

 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 <iostream>
#include <memory> // for std::shared_ptr

class Resource
{
public:
    Resource() { std::cout << "Resource acquired\n"; }
    ~Resource() { std::cout << "Resource destroyed\n"; }
};

int main()
{
    // allocate a Resource object and have it owned by std::shared_ptr
    auto ptr1 { std::make_shared<Resource>() };
    {
        auto ptr2 { ptr1 }; // create ptr2 using copy of ptr1

        std::cout << "Killing one shared pointer\n";
    } // ptr2 goes out of scope here, but nothing happens

    std::cout << "Killing another shared pointer\n";

    return 0;
} // ptr1 goes out of scope here, and the allocated Resource is destroyed

In C++ 14 onwards, you should also try to use make_shared to create objects, as it ensures better performance in memory allocation and can also solve the problems caused by the initialization order of function arguments by the compiler (consistent with the unique_ptr problem above).

enable_shared_from_this

If you accidentally return this object directly inside a class and try to get the shared_ptr of that object, it will cause an object to be deleted twice, as follows.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
class Resource
{
public:
    Resource() { std::cerr << "Resource acquired\n"; }
    ~Resource() { std::cerr << "Resource destroyed\n"; }
    std::shared_ptr<Resource> GetSPtr() {
        return std::shared_ptr<Resource>(this);
    }
};

int main()
{
    auto sptr1 = std::make_shared<Resource>();
    auto sptr2 = sptr1->GetSPtr();
    return 0;
}

The output is as follows.

1
2
3
Resource acquired
Resource destroyed
double free or corruption (out)

The above code actually generates two separate shared_ptr’s with separate control blocks, so it causes the Resource to be released twice.

Memory layout

So we use enable_shared_from_this to avoid the above situation.

1
2
3
4
5
6
7
8
9
class Resource : public std::enable_shared_from_this<Resource>
{
public:
    Resource() { std::cerr << "Resource acquired\n"; }
    ~Resource() { std::cerr << "Resource destroyed\n"; }
    std::shared_ptr<Resource> GetSPtr() {
        return shared_from_this();
    }
};

Note

The first issue is resource release. When using shared_ptr you must be careful to dispose of it properly, because it is possible that a shared_ptr is not released and thus the resources it manages are not released. With unique_ptr you only need to care if one object is freed, but with shared_ptr you need to care if all objects are freed. Take for example the following circular reference problem.

 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
#include <iostream>
#include <memory> // for std::shared_ptr
#include <string>

class Person
{
    std::string m_name;
    std::shared_ptr<Person> m_partner; // initially created empty

public:

    Person(const std::string &name): m_name(name)
    {
        std::cout << m_name << " created\n";
    }
    ~Person()
    {
        std::cout << m_name << " destroyed\n";
    }

    friend bool partnerUp(std::shared_ptr<Person> &p1, std::shared_ptr<Person> &p2)
    {
        if (!p1 || !p2)
            return false;

        p1->m_partner = p2;
        p2->m_partner = p1;

        std::cout << p1->m_name << " is now partnered with " << p2->m_name << '\n';

        return true;
    }
};

int main()
{
    auto lucy { std::make_shared<Person>("Lucy") }; // create a Person named "Lucy"
    auto ricky { std::make_shared<Person>("Ricky") }; // create a Person named "Ricky"

    partnerUp(lucy, ricky); // Make "Lucy" point to "Ricky" and vice-versa

    return 0;
}

In the above we created two Persons Lucy and Ricky, and then let their internal m_partner assign values to each other, resulting in a circular reference to both of them, neither of which can be released.

The output is as follows.

1
2
3
Lucy created
Ricky created
Lucy is now partnered with Ricky

Second, arrays are a problem. Before C++17 shared_ptr was not good at hosting arrays, so you should not use shared_ptr to host C-style arrays in C++17 and earlier. Before C++17 shared_ptr would delete the managed arrays by delete instead of delete[].

So we have two ways to solve this problem, the first is to use vector instead of new[], e.g. shared_ptr<vector<int>>; the second is a custom deleter, e.g.

1
2
3
4
5
6
7
8
template< typename T >
struct array_deleter
{
  void operator ()( T const * p)
  { 
    delete[] p; 
  }
};

Creating a shared_ptr should be written like this.

1
std::shared_ptr<int> sp(new int[10], array_deleter<int>());

Details can be found here: https://stackoverflow.com/questions/13061979/shared-ptr-to-an-array-should-it-be-used

The third is that we can convert from unique_ptr to shared_ptr, but shared_ptr is not convertible to unique_ptr. For example.

1
2
3
4
5
std::unique_ptr<std::string> unique = std::make_unique<std::string>("test");
std::shared_ptr<std::string> shared = std::move(unique);

//or
std::shared_ptr<std::string> shared = std::make_unique<std::string>("test");

std::weak_ptr

Above we introduced the problem that shared_ptr may not free objects due to circular references, and weak_ptr is designed to solve this problem. So weak_ptr is a smart pointer introduced to work with shared_ptr, it doesn’t actually manage the object, it points to an object managed by shared_ptr without affecting the life cycle of the object it refers to, i.e. binding a weak_ptr to a shared_ptr doesn’t change the reference count of the shared _ptr’s reference count.

So let’s see how to solve the circular reference problem with weak_ptr in the above example.

 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
#include <iostream>
#include <memory> // for std::shared_ptr and std::weak_ptr
#include <string>

class Person
{
    std::string m_name;
    std::weak_ptr<Person> m_partner; // note: This is now a std::weak_ptr

public:

    Person(const std::string &name): m_name(name)
    {
        std::cout << m_name << " created\n";
    }
    ~Person()
    {
        std::cout << m_name << " destroyed\n";
    }

    friend bool partnerUp(std::shared_ptr<Person> &p1, std::shared_ptr<Person> &p2)
    {
        if (!p1 || !p2)
            return false;

        p1->m_partner = p2;
        p2->m_partner = p1;

        std::cout << p1->m_name << " is now partnered with " << p2->m_name << '\n';

        return true;
    }
};

int main()
{
    auto lucy { std::make_shared<Person>("Lucy") };
    auto ricky { std::make_shared<Person>("Ricky") };

    partnerUp(lucy, ricky);

    return 0;
}

The output is as follows.

1
2
3
4
5
Lucy created
Ricky created
Lucy is now partnered with Ricky
Ricky destroyed
Lucy destroyed

Because Person uses weak_ptr internally, so in the above example, we point lucy’s m_partner to Ricky, but because it is a weak_ptr, it does not count, so it can be destroyed normally after the main method is finished.

Because weak_ptr does not implement the -> operator, so in general you cannot use it directly, so we can convert weak_ptr to shared_ptr by using the lock method of weak_ptr, 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
46
47
48
49
#include <iostream>
#include <memory> // for std::shared_ptr and std::weak_ptr
#include <string>

class Person
{
    std::string m_name;
    std::weak_ptr<Person> m_partner; // note: This is now a std::weak_ptr

public:

    Person(const std::string &name) : m_name(name)
    {
        std::cout << m_name << " created\n";
    }
    ~Person()
    {
        std::cout << m_name << " destroyed\n";
    }

    friend bool partnerUp(std::shared_ptr<Person> &p1, std::shared_ptr<Person> &p2)
    {
        if (!p1 || !p2)
            return false;

        p1->m_partner = p2;
        p2->m_partner = p1;

        std::cout << p1->m_name << " is now partnered with " << p2->m_name << '\n';

        return true;
    }

    const std::shared_ptr<Person> getPartner() const { return m_partner.lock(); } // use lock() to convert weak_ptr to shared_ptr
    const std::string& getName() const { return m_name; }
};

int main()
{
    auto lucy { std::make_shared<Person>("Lucy") };
    auto ricky { std::make_shared<Person>("Ricky") };

    partnerUp(lucy, ricky);

    auto partner = ricky->getPartner(); // get shared_ptr to Ricky's partner
    std::cout << ricky->getName() << "'s partner is: " << partner->getName() << '\n';

    return 0;
}

The output is as follows.

1
2
3
4
5
6
Lucy created
Ricky created
Lucy is now partnered with Ricky
Ricky's partner is: Lucy
Ricky destroyed
Lucy destroyed

weak_ptr can also avoid dangling pointers

Dangling pointers are objects that are freed, but the target of the pointer is not modified, resulting in a pointer to a freed piece of memory, which may result in duplicate frees and other unpredictable behavior.

For a raw pointer, we cannot know whether the address pointed to by the pointer is a dangling pointers or not, so for weak_ptr we can use the expired method to check whether the object pointed to by weak_ptr has been released or not, thus solving this problem to some extent.

Let’s look at an example.

 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
// h/t to reader Waldo for an early version of this example
#include <iostream>
#include <memory>

class Resource
{
public:
    Resource() { std::cerr << "Resource acquired\n"; }
    ~Resource() { std::cerr << "Resource destroyed\n"; }
};

// Returns a std::weak_ptr to an invalid object
std::weak_ptr<Resource> getWeakPtr()
{
    auto ptr{ std::make_shared<Resource>() };
    return std::weak_ptr{ ptr };
} // ptr goes out of scope, Resource destroyed

// Returns a dumb pointer to an invalid object
Resource* getDumbPtr()
{
    auto ptr{ std::make_unique<Resource>() };
    return ptr.get();
} // ptr goes out of scope, Resource destroyed

int main()
{
    auto dumb{ getDumbPtr() };//Returned raw pointer
    std::cout << "Our dumb ptr is: " << ((dumb == nullptr) ? "nullptr\n" : "non-null\n");

    auto weak{ getWeakPtr() };//Return weak_ptr
    std::cout << "Our weak ptr is: " << ((weak.expired()) ? "expired\n" : "valid\n");

    return 0;
}

The output is as follows.

1
2
3
4
5
6
Resource acquired
Resource destroyed
Our dumb ptr is: non-null
Resource acquired
Resource destroyed
Our weak ptr is: expired

In the above example the getDumbPtr method uses a unique_ptr to host the object and returns a pointer to the object hosted inside, so the object will be destroyed at the end of the getDumbPtr method, so getDumbPtr returns a dangling pointers and finally we find that dumb still points to an address, but the object at that address has been freed.

For the getWeakPtr method, weak_ptr is returned, but as we said above, weak_ptr does not host objects, so at the end of the getWeakPtr method, the object returned by getWeakPtr is actually destroyed by share_ptr. So in the main function weak actually points to an invalid memory region, and we can know if the object it points to has been destroyed by calling the weak.expired() method.

weak_ptr implementation cache

Because it is a separate expired operation, there may be concurrency issues, such as the object being destroyed after expired, and then using it again may produce unpredictable results. So for this kind of judgment, we can leave it to the lock function. If the weak_ptr is not destroyed, then it will return shared_ptr, which is equivalent to extending its lifetime because it increments a reference count, and if the weak_ptr has been destroyed, then it will return nil.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
// Returns a std::weak_ptr to an invalid object
std::weak_ptr<Resource> getWeakPtr()
{
    auto ptr{ std::make_shared<Resource>() };
    return std::weak_ptr{ ptr };
} // ptr goes out of scope, Resource destroyed

int main()
{
    std::shared_ptr<Resource> spw = getWeakPtr().lock(); //return null
    std::cout << "Our share ptr is: " << ((spw == nullptr) ? "nullptr\n" : "non-null\n");
    std::shared_ptr<Resource> spw2(getWeakPtr()); //if object is expired,throw std::bad_weak_ptr
    return 0;
}

The output is as follows.

1
2
3
4
5
6
7
Resource acquired
Resource destroyed
Our share ptr is: nullptr
Resource acquired
Resource destroyed
terminate called after throwing an instance of 'std::bad_weak_ptr'
  what():  bad_weak_ptr

In the above example, spw will return null and spw2 will throw an exception. So with lock we can implement cache operations. For example, if we have a global map as a cache, and the value inside is weak_ptr, then each time we get the cache, we can determine whether the value returned by lock is null, and return it directly if it is not null, and reload the cache if it is null.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
std::shared_ptr<const Widget> fastLoadWidget(WidgetID id)
{
    static std::unordered_map<WidgetID,std::weak_ptr<const Widget>> cache;
    // objPtr is std::shared_ptr to cached object (or null
    // if object's not in cache)
    auto objPtr = cache[id].lock(); 

    if (!objPtr) { // if not in cache,
        objPtr = loadWidget(id); // load it
        cache[id] = objPtr; // cache it
    }
    return objPtr;
}

Summary

unique_ptr It is a pointer that takes exclusive ownership of a resource. unique_ptr will be allocated on the stack and then freed after leaving the scope, deleting the Resource object held inside, and it can only transfer objects using move semantics. So if you want to manipulate a pointer, allocate memory on entry to the scope and then safely release the object when you leave the scope, then you can use it.

shared_ptr It manages resources that can be held by multiple objects and uses a reference counting strategy to release objects; if the count is not cleared, then the resources it manages will not be released.

weak_ptr It does not manage objects, but is only an observer of the resources managed by the shared_ptr object, so it does not affect the lifecycle of shared resources, it is generally used in caching, resolving shared_ptr circular references, etc.

Ref

  • https://www.luozhiyun.com/archives/762