make_shared, weak_ptr and Control Block

I added weak ptr functionality and make_shared
to my C++ smart pointer sandbox and thought I'd share the
core ideas here. Also as a side note, I think most text you see online these days are AI generated and it feels a little
dull — so from now on, everything you see here or on my blog will be fully my words. Please comment or PM me if you
disagree with anything or if I missed out on anything!
1. Weak Ptr
-
The primary use case of
weak_ptr
is to avoid memory leaks in the case of cyclic references, e.g. A <-> B. The strong reference counts never reach 0 even when both objects go out of scope and the shared pointer is never deleted, but usingweak_ptr
for one direction breaks this cycle. -
I abstracted away a control block struct which contains strong counts and weak counts, and both
weak_ptr
andshared_ptr
use it. -
When the strong count to a certain shared pointer reaches 0, the managed object is deleted and
ptr
is set tonullptr
, but the control block remains. - Only when the weak pointer count reaches 0, the control block itself is destroyed.
- This is to prevent use-after-free, as the weak ptr should be able to access the control block and safely check the object's lifetime as long as it is still alive.
-
If I wish to access the object using the weak pointer, I have to use the
.lock()
method, which creates a new shared pointer pointing to that object.-
If the object is deleted already, it will simply return a shared pointer pointing to
null
(preventing use-after-free). -
This is usually wrapped within a
so the newly created shared pointer goes out of scope when whatever you wanna do with it is done.
-
If the object is deleted already, it will simply return a shared pointer pointing to
2. make_shared
- Takes in arguments and perfectly forwards (if lvalue is passed in, invokes copy ctor; if rvalue is passed in, invokes move ctor — can even mix and match!! These people are so smart honestly... ) those arguments to create a new pointer to the object with the control block, at the same time.
-
To facilitate this, I created an
InlineControlBlock
struct that has a buffer as a field and stores the object within the buffer, so the object itself is stored within this struct.
Advantages
- Single Memory Allocation:
- Usually one allocation is made for the pointer and another is made for control block, but here only one is made.
- Fewer allocations means less memory accesses and it's faster.
- Object and control block are adjacent in memory, reducing cache misses.
-
Exception safety (this seems to be a core theme in C++,
copy-and-swap
during copy assignment also does this) — if initializing the control block throws but initializing the raw pointer does not, there would be a raw pointer now all by its lonesome :/
3. Memory Alignment Requirements
When creating my object of type U
within the buffer inside the InplaceControlBlock
struct,
I used alignas(U)
. This ensures that the object adheres to the memory alignment requirements of type U
. This got me thinking — why do certain objects require alignment to a boundary of a certain size?
- ABI compliance
- Hardware requirements
- Many atomic operations require proper alignment
And more importantly:
Cache Line Efficiency
- Misaligned data can span multiple cache lines.
- When data spans across 2 cache lines: higher cache miss rates, multiple memory accesses required, need to invalidate/validate multiple cache lines.
- Increased false sharing between CPU cores.