The key is separating the pointer handle from the heap object it points to.
} the structs are destroyed, so their shared_ptrs die too — why doesn't it work?"
At }, only the stack handles (main's local a and b) are destroyed. Each one dying just decrements a count by 1 — it deletes the heap object only if the count hits 0.
The other two shared_ptrs (A.b and B.a) live inside the heap objects, not on the stack. A heap object is destroyed only when its count reaches 0 — but that can't happen until the other object is destroyed first. Each keeps the other alive → nobody reaches 0.
}A is referenced by 2 pointers → count 2. Same for B.
} (all shared) leakStuck. Each count dropped 2 → 1 (the stack handles are gone), but each object is still held by the other object's member. Neither reaches 0, so neither destructor runs, so those member pointers never die either. Both objects are unreachable but alive forever → memory leak.
Cascade of cleanup: at } → main.a dies → A count 1 → 0 → A destroyed → A.b dies → B count 2 → 1 → main.b dies → B count 1 → 0 → B destroyed. No leak.
A shared_ptr stored inside an object keeps the target alive as long as that object lives — and if two objects do this to each other, neither can ever die. weak_ptr observes without counting, so it breaks the deadlock.