Fundamentals 10 min read

Why Misusing std::shared_ptr Slows Your C++ Code and How to Fix It

This article explores common pitfalls when using std::shared_ptr in C++, including unnecessary copying, extra heap allocations, cyclic references, and subtle memory retention with make_shared, provides benchmark comparisons, and offers practical guidelines such as preferring const‑reference passing, using std::make_shared, and employing weak_ptr to break cycles.

IT Services Circle
IT Services Circle
IT Services Circle
Why Misusing std::shared_ptr Slows Your C++ Code and How to Fix It

History

std::shared_ptr originated in Boost in 1999, long before it was adopted by the C++ standard. The only smart pointer in early C++ was auto_ptr, which had problematic copy semantics and was deprecated in C++11 and removed in C++17. In C++11, boost::shared_ptr was incorporated into the standard as std::shared_ptr and has become the most widely used smart pointer.

How shared_ptr Works

Internally a shared_ptr holds two pointers: one to the managed object and one to a control block that stores the reference count and other metadata. Its lifetime rules are:

Copy‑construction increments the reference count.

Assignment increments the right‑hand side count and decrements the left‑hand side count.

Destruction decrements the reference count.

When the count reaches zero, the managed object is destroyed.

Example 1 – Passing by Value vs. by Reference

#include <memory>
#include <chrono>
#include <iostream>
using shared_ptr_t = std::shared_ptr<int>;

void receiver_by_value(shared_ptr_t ptr) {
    volatile int x = *ptr;
    (void)x;
}

void receiver_by_ref(const shared_ptr_t& ptr) {
    volatile int x = *ptr;
    (void)x;
}

void test_by_value(uint64_t n) {
    auto ptr = std::make_shared<int>(100);
    for (uint64_t i = 0; i < n; ++i) {
        receiver_by_value(ptr);
    }
}

void test_by_ref(uint64_t n) {
    auto ptr = std::make_shared<int>(100);
    for (uint64_t i = 0; i < n; ++i) {
        receiver_by_ref(ptr);
    }
}

int main() {
    uint64_t n = 1000000;
    {
        auto start = std::chrono::high_resolution_clock::now();
        test_by_value(n);
        auto end = std::chrono::high_resolution_clock::now();
        std::cout << std::chrono::duration_cast<std::chrono::microseconds>(end - start).count() << " us
";
    }
    {
        auto start = std::chrono::high_resolution_clock::now();
        test_by_ref(n);
        auto end = std::chrono::high_resolution_clock::now();
        std::cout << std::chrono::duration_cast<std::chrono::microseconds>(end - start).count() << " us
";
    }
}

On a typical machine the value‑passing version takes roughly 30 ms, while the reference‑passing version takes about 2 ms. Copying a shared_ptr therefore adds measurable overhead; pass by const reference unless ownership transfer is required.

Example 2 – make_shared vs. Manual Allocation

// Manual allocation (two heap allocations)
auto ptr = std::shared_ptr<int>(new int(42));

This performs two separate heap allocations: one for the int and one for the control block, increasing allocation cost and reducing cache locality.

// Preferred allocation (single allocation)
auto ptr = std::make_shared<int>(42);

Using std::make_shared allocates the object and its control block in a single memory block. A valgrind test creating 100 000 objects shows ~200 003 allocations with the manual approach versus ~100 003 allocations with make_shared, confirming the efficiency gain.

Example 3 – Cyclic References

struct B;
struct A { ~A(){ std::cout << "~A
"; } std::shared_ptr<B> b; };
struct B { ~B(){ std::cout << "~B
"; } std::shared_ptr<A> a; };

void test() {
    auto a = std::make_shared<A>();
    auto b = std::make_shared<B>();
    a->b = b;
    b->a = a;
}

The two objects hold shared_ptr references to each other, forming a reference‑count cycle. When test returns, both reference counts remain non‑zero, so the destructors never run and memory leaks.

Break the cycle by replacing one side with std::weak_ptr:

struct B;
struct A { ~A(){ std::cout << "~A
"; } std::shared_ptr<B> b; };
struct B { ~B(){ std::cout << "~B
"; } std::weak_ptr<A> a; };

Access the object via weak_ptr::lock() and always check the returned shared_ptr for null before use.

Example 4 – Interaction of make_shared and weak_ptr

When make_shared stores the object and control block together, the memory cannot be released as long as any weak_ptr still points to the control block, even after the last shared_ptr is destroyed. This can keep large objects alive unintentionally.

If any std::weak_ptr references the control block created by std::make_shared after the lifetime of all shared owners ended, the memory occupied by T persists until all weak owners are destroyed as well, which may be undesirable if sizeof(T) is large.
std::weak_ptr<LargeType> weakPtr;
{
    auto sharedPtr = std::make_shared<LargeType>();
    weakPtr = sharedPtr;
    // ...
} // LargeType's memory is still retained because weakPtr exists.

By contrast, allocating with new and constructing a shared_ptr manually separates the object from the control block, allowing the memory to be released when the last shared_ptr goes out of scope, even if a weak_ptr remains.

std::weak_ptr<LargeType> weakPtr;
{
    auto sharedPtr = std::shared_ptr<LargeType>(new LargeType);
    weakPtr = sharedPtr;
    // ...
} // Memory is freed here because the control block is separate.

Practical Recommendations

Pass std::shared_ptr by const reference unless ownership transfer is needed.

Prefer std::make_shared for object creation to avoid extra allocations and improve cache locality.

Break reference cycles with std::weak_ptr and always check the result of weak_ptr::lock() before dereferencing.

Be aware that a lingering weak_ptr can keep the memory of a make_shared object alive after all owners are gone; for large objects where deterministic deallocation is required, consider manual allocation (separate control block).

Original Source

Signed-in readers can open the original source through BestHub's protected redirect.

Sign in to view source
Republication Notice

This article has been distilled and summarized from source material, then republished for learning and reference. If you believe it infringes your rights, please contactadmin@besthub.devand we will review it promptly.

performanceCshared_ptrweak_ptrmemory-management
IT Services Circle
Written by

IT Services Circle

Delivering cutting-edge internet insights and practical learning resources. We're a passionate and principled IT media platform.

0 followers
Reader feedback

How this landed with the community

Sign in to like

Rate this article

Was this worth your time?

Sign in to rate
Discussion

0 Comments

Thoughtful readers leave field notes, pushback, and hard-won operational detail here.