std::shared_ptr

The Spirit Of std::shared_ptr

  • Shared ownership: multiple pointer objects reference one object that is shared

  • Pointer copy creates one more reference

    • ⟶ Increases reference count

  • Last remaining reference is responsible for deallocation

Notes About Thread Safety

  • Reference count is thread-safe

  • ⟶ Creating new references (i.e. copying pointer objects) is a little more expensive than a simple integer increment

  • Pointer object itself is not thread-safe

  • ⟶ Concurrent writes lead to undefined behavior

  • See Atomic Shared Pointer (std::atomic<std::shared_ptr<>) for a “workaround”

Creating std::shared_ptr Instances

  • Raw pointer at the basis

  • std::shared_ptr<> wrapped around

  • ⟶ “Control block” created: reference count

  • Reference count initialized to one

#include "big-data.h"
#include <gtest/gtest.h>
#include <memory>

TEST(shared_ptr_suite, basic_creation)
{
    BigData* data = new BigData(100, 'a');
    std::shared_ptr<BigData> pdata(data);
    ASSERT_EQ(pdata.use_count(), 1);                   // <-- pdata is the only reference
}
../../../../../../_images/sharedptr.svg
  • More condensed usage (just like std::unique_ptr<>)

  • Convenience function: allocates object of type T, and forward arguments to constructor

#include "big-data.h"
#include <gtest/gtest.h>
#include <memory>

TEST(shared_ptr_suite, more_condensed_creation)
{
    auto pdata = std::make_shared<BigData>(100, 'a');
}

Copying std::shared_ptr Instances

  • Copying is what shared pointers are there for

  • Each pointer copy adds one reference to the object

#include "big-data.h"
#include <gtest/gtest.h>
#include <memory>

TEST(shared_ptr_suite, copy)
{
    auto pdata = std::make_shared<BigData>(100, 'a');
    auto copy = pdata;
    ASSERT_EQ(pdata.get(), copy.get());                // <-- both point to same object
    ASSERT_EQ(pdata.use_count(), 2);                   // <-- pointed-to object has one more reference
}
../../../../../../_images/sharedptr-copy.svg

Object Lifetime (“Garbage Collection”)

How long does the pointed-to object live?

  • Reference count is used to track shared ownership

  • When reference count drops to zero, the object is not referenced anymore

  • ⟶ deallocated

Examining the reference count

  • The zero-transition of the reference count is the only event in the object lifecycle that can be counted on

  • Everything else is dangerous when the object is shared across threads

#include "big-data.h"
#include <gtest/gtest.h>
#include <memory>

TEST(shared_ptr_suite, refcount_if)
{
    auto pdata = std::make_shared<BigData>(100, 'a');
    auto copy = pdata;
    if (pdata.use_count() == 2)                        // <-- there be dragons!
        /*do something*/;
}

Raw Pointer Access: .get()

  • Not the plan

  • Sometimes needed though

  • ⟶ Careful!

#include "big-data.h"
#include <gtest/gtest.h>
#include <memory>

TEST(shared_ptr_suite, raw_pointer_get)
{
    auto pdata = std::make_shared<BigData>(100, 'a');
    BigData* raw = pdata.get();
    ASSERT_EQ(raw->at(42), 'a');                       // <-- operator->()
    ASSERT_EQ((*raw).at(42), 'a');                     // <-- operator*()
    ASSERT_EQ(raw, pdata.get());                       // <-- .get() leaves pdata untouched
}

Unreferencing Objects: .reset()

  • Nulling out pointer object

  • Drop reference on object (decrease reference count)

#include "big-data.h"
#include <gtest/gtest.h>
#include <memory>

TEST(shared_ptr_suite, reset)
{
    auto pdata = std::make_shared<BigData>(100, 'a');
    auto copy = pdata;                                 // <-- create second reference

    pdata.reset();                                     // <-- drop one reference on original
    ASSERT_EQ(pdata, nullptr);
    ASSERT_EQ(copy.use_count(), 1);

    copy.reset();                                      // <-- drop last reference (deallocated object)
}
  • Variant: exchanging pointer-to object with different (or even same) object

#include "big-data.h"
#include <gtest/gtest.h>
#include <memory>

TEST(shared_ptr_suite, reset_exchange)
{
    auto pdata = std::make_shared<BigData>(100, 'a');
    auto copy = pdata;

    BigData* newdata = new BigData(1000, 'b');
    pdata.reset(newdata);                              // <-- unref original, ref new
    ASSERT_EQ(pdata.get(), newdata);
    ASSERT_EQ(copy.use_count(), 1);
}

Custom Deleter

  • Deleter not part of the type - as opposed to std::unique_ptr

  • ⟶ would prevent sharing, obviously

  • Delete stored in control block

  • ⟶ Each shared object can have its own deleter

  • .get_deleter()

#include "big-data.h"
#include <gtest/gtest.h>
#include <memory>

TEST(shared_ptr_suite, deleter)
{
    bool deleted1 = false;
    auto deleter1 = [&deleted1](BigData* obj) {
        deleted1 = true;
        delete obj;
    };
    std::shared_ptr<BigData> pdata(new BigData(100, 'a'), deleter1); // <-- ctor parameter

    bool deleted2 = false;
    auto deleter2 = [&deleted2](BigData* obj) {
        deleted2 = true;
        delete obj;
    };
    BigData* newdata = new BigData(100, 'b');

    pdata.reset(newdata, deleter2);                    // <-- unref (and delete) original, ref new
    ASSERT_TRUE(deleted1);

    pdata.reset();                                     // <-- unref (and delete) new data
    ASSERT_TRUE(deleted2);
}

Cyclic References

  • Cyclic references are not detected

  • Memory leak

#include <memory>

class NonSense
{
public:
    void set_reference(std::shared_ptr<NonSense> ref)
    {
        _ref = ref;
    }

private: 
    std::shared_ptr<NonSense> _ref;
};

int main()
{
    std::shared_ptr<NonSense> cycle(new NonSense);
    cycle->set_reference(cycle);
    return 0;
}
$ valgrind ./c++11-shared-ptr-cyclic
...
==888857== HEAP SUMMARY:
==888857==     in use at exit: 40 bytes in 2 blocks
==888857==   total heap usage: 3 allocs, 1 frees, 73,768 bytes allocated
==888857==
==888857== LEAK SUMMARY:
==888857==    definitely lost: 16 bytes in 1 blocks
==888857==    indirectly lost: 24 bytes in 1 blocks
==888857==      possibly lost: 0 bytes in 0 blocks
==888857==    still reachable: 0 bytes in 0 blocks
==888857==         suppressed: 0 bytes in 0 blocks
...