63 Intermediate The Hundred
The Reference Count That Never Reached Zero
two objects, each politely waiting for the other to leave first
struct Node {
std::string name;
std::shared_ptr<Node> child;
std::shared_ptr<Node> parent;
explicit Node(std::string n) : name(std::move(n)) {}
~Node() { std::cout << "destroying " << name << "\n"; }
};
int main() {
{
auto root = std::make_shared<Node>("root");
auto leaf = std::make_shared<Node>("leaf");
root->child = leaf;
leaf->parent = root; // every child should know its parent
std::cout << "root use_count " << root.use_count() << ", leaf use_count "
<< leaf.use_count() << "\n";
}
std::cout << "scope closed\n";
}
Run it. Which destructors run when the inner scope ends?
Answer
None of them. Two smart pointers, two nodes, zero destructors:
Why¶
Each make_shared starts a count at 1; then root->child = leaf lifts leaf to 2 and
leaf->parent = root lifts root to 2 — that pair of twos, printed a line before it bites,
is the bug. At scope exit the locals each release one reference, leaving root at 1 (held
by leaf->parent) and leaf at 1 (held by root->child): neither hits zero, so neither
destructor runs and the pair keeps itself alive with nobody left who can reach it.
Reference counting is a purely local rule — it can tell that nothing points at one
object, never that nothing points into a group. Not undefined behavior, and
-Wall -Wextra are silent (GCC 11.5) — just a quiet leak, which valgrind
--leak-check=full reports as 160 (80 direct, 80 indirect) bytes ... definitely lost.
The fix¶
Let the back-pointer observe instead of own — a weak_ptr adds nothing to the count, and
.lock() hands you a shared_ptr on the occasions you really need the parent:
Change that one line and root's count prints as 1; at scope exit root is destroyed first,
and releasing its child takes leaf with it. Owning pointers must form a tree.
Takeaway: reference counting cannot collect cycles — break them by design, with
weak_ptr on the links that point back.
Try it: g++ -std=c++17 main.cpp -o demo && ./demo