Contact
Evil Genius SocietyHomeLearnForumLeaderboardSocialsMessages
← Back to module

Lesson 3 of 6

std::shared_ptr<T> allows several owners. The object is destroyed when the last of them goes away.

#include <iostream>
#include <memory>

struct Resource {
    int id;
    Resource(int i) : id(i) { std::cout << "  acquire " << id << "\n"; }
    ~Resource() { std::cout << "  release " << id << "\n"; }
};

int main() {
    auto a = std::make_shared<Resource>(1);
    std::cout << "owners: " << a.use_count() << "\n";

    {
        auto b = a;                       // copying is allowed — both own it
        std::cout << "owners: " << a.use_count() << "\n";
        std::cout << "same object: " << (a.get() == b.get()) << "\n";
    }   // b goes away, count drops

    std::cout << "owners: " << a.use_count() << "\n";
    std::cout << "leaving main\n";
    return 0;
}
  acquire 1
owners: 1
owners: 2
same object: 1
owners: 1
leaving main
  release 1

One object, two owners at the peak, destroyed exactly once when the last one left.

How it works

A shared_ptr holds two pointers: one to the object, one to a control block containing the reference counts.

   a               b
   |               |
   +-------+-------+
           |
      control block          the object
   +----------------+     +------------+
   | strong count 2 |     | Resource 1 |
   | weak count   0 |     +------------+
   +----------------+

Copying increments the strong count; destroying a copy decrements it. At zero, the object is destroyed. Lesson 10.4 explains the weak count.

That is why a shared_ptr is twice the size of a raw pointer:

#include <iostream>
#include <memory>

int main() {
    std::cout << "raw:        " << sizeof(int*) << "\n";
    std::cout << "unique_ptr: " << sizeof(std::unique_ptr<int>) << "\n";
    std::cout << "shared_ptr: " << sizeof(std::shared_ptr<int>) << "\n";
    return 0;
}
raw:        8
unique_ptr: 8
shared_ptr: 16

The costs, stated honestly

shared_ptr is not free, and the costs are worth knowing before you reach for it:

The counter is atomic. Increments and decrements must be thread-safe, because copies may live on different threads. Atomic operations are considerably more expensive than ordinary ones, and copying a shared_ptr in a hot loop is a real performance problem.

It is two allocations if you use new. std::shared_ptr<T>(new T) allocates the object and then the control block separately. std::make_shared<T>() combines them into one — lesson 10.5.

Only the control block is thread-safe. Two threads may safely copy and destroy shared_ptrs to the same object. They may not safely read and write the object without synchronisation. The counter being atomic protects the bookkeeping, not your data. Module 21 covers the rest.

It obscures the design. A shared_ptr in a signature says "the lifetime here is complicated." If it is not complicated, saying so is misleading.

Where it earns its keep

The honest case is a resource whose last user is not knowable in advance:

#include <iostream>
#include <memory>
#include <string>
#include <vector>

struct Texture {
    std::string name;
    Texture(std::string n) : name(std::move(n)) {
        std::cout << "  loaded " << name << "\n";
    }
    ~Texture() { std::cout << "  freed " << name << "\n"; }
};

struct Sprite {
    std::string label;
    std::shared_ptr<Texture> texture;      // shares ownership
};

int main() {
    std::vector<Sprite> sprites;

    {
        auto stone = std::make_shared<Texture>("stone.png");

        sprites.push_back(Sprite{"wall", stone});
        sprites.push_back(Sprite{"floor", stone});
        std::cout << "owners of stone.png: " << stone.use_count() << "\n";
    }   // the local `stone` is gone, but the sprites keep it alive

    std::cout << "after the loader scope, sprites still work\n";
    std::cout << sprites[0].label << " uses " << sprites[0].texture->name << "\n";

    sprites.pop_back();
    std::cout << "owners now: " << sprites[0].texture.use_count() << "\n";

    std::cout << "clearing sprites:\n";
    sprites.clear();
    std::cout << "done\n";
    return 0;
}
  loaded stone.png
owners of stone.png: 3
after the loader scope, sprites still work
wall uses stone.png
owners now: 1
clearing sprites:
  freed stone.png
done

The texture outlived the scope that loaded it, was shared by two sprites, and was freed exactly when the last sprite went away. No code decided when to free it, because no code could — that is the situation shared_ptr is for.

Note the count of 3 at the peak: the local stone plus the two sprites.

use_count is for debugging only

It tells you the count at the moment you asked, which in a threaded program is already historical. Never branch on it:

if (p.use_count() == 1) {
    // By the time this runs, another thread may have made a copy.
}

Print it while learning; do not build logic on it.

Passing them around

The common mistake is taking shared_ptr by value everywhere, which copies the atomic counter on every call for no reason:

#include <iostream>
#include <memory>
#include <string>

struct Config {
    std::string name;
};

// Wrong by default: copies the shared_ptr, touching the atomic counter,
// when the function does not participate in ownership at all.
void bad(std::shared_ptr<Config> c) {
    std::cout << "bad:  " << c->name << " (count " << c.use_count() << ")\n";
}

// Right: this function just reads the object.
void good(const Config& c) {
    std::cout << "good: " << c.name << "\n";
}

// Right when the function genuinely stores a new owner.
void takes_ownership(std::shared_ptr<Config> c) {
    std::cout << "kept: " << c->name << " (count " << c.use_count() << ")\n";
}

int main() {
    auto config = std::make_shared<Config>(Config{"settings.ini"});

    bad(config);
    good(*config);
    takes_ownership(config);

    std::cout << "final count: " << config.use_count() << "\n";
    return 0;
}
bad:  settings.ini (count 2)
good: settings.ini
kept: settings.ini (count 2)
final count: 1

The rule: take const T& unless the function stores an owner. A shared_ptr parameter should mean "I am keeping this."

Try it

Model a library where several Readers borrow the same Book. Use shared_ptr and print the count as readers appear and finish. Then rewrite it so the library owns the books with unique_ptr and readers hold Book*, and say which design you prefer.

Checkpoint

  • std::shared_ptr<T> allows several owners; the object dies with the last one.
  • It holds a pointer to the object plus one to a control block of reference counts.
  • It is twice the size of a raw pointer, and the counter is atomic, so copies are not cheap.
  • The control block is thread-safe; the object is not.
  • use_count() is a debugging aid — never branch on it.
  • Take const T& in functions that only read; take shared_ptr only to store an owner.
  • Use it when the last owner genuinely cannot be named in advance.

Questions about this lesson

Log in to ask a question.