shared_ptr(this) Trap: The Owner That Splits Into Two2026-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.
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.
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:
shared_from_this() from a constructor or destructor. The internal weak_ptr is set up after the constructor returns and expires before the destructor runs. Calling it too early throws std::bad_weak_ptr; too late is undefined.shared_ptr. If someone allocated your Task on the stack or with new without wrapping it, shared_from_this() throws. Enforce ownership by making the constructor private and exposing a static create() factory returning shared_ptr<Task>.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.
