C++'s shared_ptr(this) Trap: The Owner That Splits Into Two

2026-07-04

This scheduler stores tasks in a shared queue so a worker thread can pick them up later. Each Task knows how to enqueue itself. The queue owns a shared_ptr<Task>, and callers also hold their own shared_ptr<Task> to inspect the task after scheduling. Reference counting will keep it alive until both are done. What could go wrong?

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

class Task {
public:
    void schedule(std::vector<std::shared_ptr<Task>>& queue) {
        // Register ourselves with the run queue.
        queue.push_back(std::shared_ptr<Task>(this));
    }
    void run() const { std::cout << "Task " << id_ << " running\n"; }
private:
    int id_ = 42;
};

int main() {
    std::vector<std::shared_ptr<Task>> queue;
    auto t = std::make_shared<Task>();
    t->schedule(queue);
    queue.front()->run();          // works fine
    return 0;                      // then: double-free, likely SIGABRT
}

Under ASan or a debug allocator you'll see attempting free on address which was not malloc()-ed, or the classic double free detected in tcache 2. In release builds it might silently corrupt the heap and crash later, in a totally unrelated allocation.

The Bug

A shared_ptr's reference count lives in a control block that is allocated when the shared_ptr is first constructed from a raw pointer. std::make_shared<Task>() allocated one control block. Then inside schedule, the expression std::shared_ptr<Task>(this) constructed a second, independent control block wrapping the same raw Task*.

Now the object has two owners that don't know about each other. Each control block has its own refcount of 1. When queue is destroyed, its shared_ptr hits zero and calls delete. When t is destroyed, its control block also hits zero and calls delete on the already-freed pointer. Undefined behavior.

The same trap fires anytime a member function needs to hand a shared_ptr to something outside itself — a callback registration, a completion handler passed to boost::asio, a worker queue. It's especially seductive because the code looks obviously right: "I have this, and I need a shared_ptr<T>, so I'll construct one." The compiler won't stop you.

The Fix

Inherit from std::enable_shared_from_this<T> and call shared_from_this(). When the object was created via make_shared (or any shared_ptr constructor), that base class stashes a weak_ptr pointing at the original control block. shared_from_this() upgrades that weak_ptr, giving you a shared_ptr that shares the correct refcount.

class Task : public std::enable_shared_from_this<Task> {
public:
    void schedule(std::vector<std::shared_ptr<Task>>& queue) {
        queue.push_back(shared_from_this());   // shares the existing control block
    }
    void run() const { std::cout << "Task " << id_ << " running\n"; }
private:
    int id_ = 42;
};

Two caveats that will bite you next:

Key Takeaway: Constructing a shared_ptr from a raw this spawns a second control block and turns shared ownership into a double-free — always use enable_shared_from_this and shared_from_this() instead.

All newsletters