Smart Pointers

01 Mar 2017 in slides
Slide Show

Smart Pointers

Contents

  • What is a smart pointer
  • std::auto_ptr
  • std::unique_ptr
  • reference counting
  • std::shared_ptr
  • other smart pointers

What is a smart pointer

Object that behaves as a pointer by overloading operator*() and operator->()

// the simplest SmartPointer
template <typename T>
class SmartPointer
{
    public:
        T* operator->()
        {
            return m_Pointer;
        }

        T& operator* ()
        {
            return *m_Pointer;
        }
    private:
        T* m_Pointer;
};

operator->()

Every time the compiler evaluates the -> operator, until the right side of the operator becomes a native pointer.

What are smart pointers useful for?

  • Managing resources
  • Expressing ownership semantic

Standard smart pointers

  • std::auto_ptr
  • std::unique_ptr
  • std::shared_ptr
  • std::weak_ptr
  • boost::intrusive_ptr

std::auto_ptr

A smart pointer that frees the memory upon destruction. In C++ 11 it was removed from the language. It is fully replaced by std::unique_ptr without any drawbacks

A simplified implementation of std::auto_ptr

The "smart pointer" part

template <typename T>
class auto_ptr {
// ...
T* operator->()
{
	return m_Pointer;
}

T& operator* ()
{
	return *m_Pointer;
}
// ...
T* m_Pointer;
};

The RAII part

template <typename T>
class auto_ptr {
// ...
auto_ptr()
	: m_Pointer(nullptr)
{}

auto_ptr(T* pointer)
	: m_Pointer(pointer)
{}

~auto_ptr()
{
	delete m_Pointer;
}
// ...
T* m_Pointer;
};

The bad parts

template <typename T>
class auto_ptr {
// ...
// copy ctor changes the original!!!
auto_ptr(auto_ptr& o) : m_Pointer(o.m_Pointer) {
	o.m_Pointer = nullptr;
}

// assignment changes the original!!!
auto_ptr& operator=(auto_ptr& rhs) {
	if (this != &rhs) {
		delete m_Pointer;
		m_Pointer = rhs.m_Pointer;
		rhs.m_Pointer = nullptr;
	}
	return *this;
}
// ...
T* m_Pointer;
};

Due to the lack of move semantics in C++ 98, std::auto_ptr is forced to transfer the ownership of the pointer during copy construction and assignment. std::auto_ptr was also copiable and assignable. You could make a std::vector<std::auto_ptr>, but it would be completely unpredictable - the standard does not require certain copy/assignment guarantees for containers.

std::unique_ptr

C++ 11 introduces std::unique_ptr

  • it is not copyable
  • it is moveable

A simplified implementation of std::unique_ptr

The "smart pointer" part

template <typename T>
struct unique_ptr {
// ...
T* operator->()
{
    return m_Pointer;
}

T& operator* ()
{
    return *m_Pointer;
}
// ...
T* m_Pointer;
};

The RAII part

template <typename T>
struct unique_ptr {
// ...
unique_ptr()
    : m_Pointer(nullptr)
{}

unique_ptr(T* pointer)
    : m_Pointer(pointer)
{}

~unique_ptr()
{
    delete m_Pointer;
}
// ...
T* m_Pointer;
};

The non-copyable part

template <typename T>
struct unique_ptr {
// ...
unique_ptr(const unique_ptr& o) = delete;
unique_ptr& operator= (const unique_ptr& rhs) = delete;
// ...
T* m_Pointer;
};

The move part

template <typename T>
struct unique_ptr {
// ...
unique_ptr(unique_ptr&& o)
    : m_Pointer(o.m_Pointer)
{
    o.m_Pointer = nullptr;
}

unique_ptr& operator=(unique_ptr&& rhs)
{
    if (this != &rhs) {
        delete m_Pointer;
        m_Pointer = rhs.m_Pointer;
        rhs.m_Pointer = nullptr;
    }
    return *this;
}
// ...
T* m_Pointer;
};

std::unique_ptr<T> synopsis

  • unique_ptr() default constructor
  • unique_ptr(Y*) constructor - allows using pointers to Derived classes to be hold in std::unique_ptr<Base>
  • T& operator* ()
  • T* operator-> ()
  • const T& operator* () const
  • const T* operator-> () const
  • T* get() - get the underlying pointer
  • const T* get() const
  • T* release() - get the underlying pointer and release ownership over it. Use it when you need to give up the ownership of the resource

  • void reset(T* new_pointer = nullptr) - reset the underlying pointer to new_pointer and get ownership over it. Deletes the current pointer.

explicit operator bool() allows usage such as

std::unique_ptr<T> p;
if (p) {
    std::cout << *p << std::endl;
}

std::unique_ptr has a second template parameter

template <typename T, typename Deleter = std::default_delete<T>>
class unique_ptr
{
    ~unique_ptr() {
        auto deleter = get_deleter();
        deleter(m_Pointer);
    }
};

Bad

std::unique_ptr<Lifetime> a(new Lifetime[2]);
//std::cout << "Second: " << &a[1] << std::endl;

Will make a delete to memory allocated with new []

Good

std::unique_ptr<Lifetime[]> a(new Lifetime[2]);
std::cout << "Second: " << &a[1] << std::endl;

T& operator[](size_t index) - allows array element access. Only std::unique_ptr<T[]> has this method.

Custom deleter

struct ReleaseDelete {
    template <typename T>
    void operator()(T* pointer) {
        pointer->Release();
    }
};

// ...
std::unique_ptr<ID3D11Device, ReleaseDelete> device;

Will be back to unique_ptr deleter when we get to:

  • template typedefs
  • template specialization

std::unique_ptr owns the resource it is pointing to

Use this to express ownership relation between types

std::shared_ptr

std::shared_ptr allows sharing of a resource. It is freed when the last std::shared_ptr instance pointing to the resource is destroyed. std::shared_ptr uses Reference Counting.

Reference counting

A technique of storing the number of references, pointers, or handles to a resource.

A garbage collection algorithm that uses these reference counts to deallocate objects which are no longer referenced

Reference counting

Reference counting

Reference counting

std::shared_ptr<int> t(new int(42));// pointer 0xff00 : rc 1
std::shared_ptr<int> q = t;         // pointer 0xff00 : rc 2
std::shared_ptr<int> p(new int(24));// pointer 0xfe00 : rc 1

t = p;  // pointer 0xff00 : rc 1
        // pointer 0xfe00 : rc 2
        
q = t;  // pointer 0xff00 : rc 0, so it is deleted
        // pointer 0xfe00 : rc 3

Implementation of std::shared_ptr

The reference count and the resource need to be shared between all instances. So they have to be on the heap.

SharedPtr()
    : m_RC(nullptr)
    , m_Pointer(nullptr)
{}

SharedPtr(T* pointer)
    : m_RC(new int(1))
    , m_Pointer(pointer)
{
}
// ...
~SharedPtr()
{
    RemoveReference();
}
// ...
int* m_RC;
T* m_Pointer;
void RemoveReference()
{
    if (m_RC && --*m_RC == 0)
    {
        delete m_Pointer;
        delete m_RC;
    }
}
void AddReference()
{
    ++*m_RC;
}
SharedPtr(const SharedPtr& o)
    : m_RC(o.m_RC)
    , m_Pointer(o.m_Pointer)
{
    AddReference();
}

SharedPtr& operator=(const SharedPtr& rhs)
{
    if (this != &rhs)
    {
        RemoveReference();
        m_RC = rhs.m_RC;
        m_Pointer = rhs.m_Pointer;
        AddReference();
    }
    return *this;
}
SharedPtr(SharedPtr&& o)
    : m_RC(o.m_RC)
    , m_Pointer(o.m_Pointer)
{
    o.m_RC = nullptr;
    o.m_Pointer = nullptr;
}

SharedPtr& operator=(SharedPtr&& rhs)
{
    if (this != &rhs) {
        RemoveReference();
        m_RC = rhs.m_RC;
        m_Pointer = rhs.m_Pointer;
        rhs.m_RC = nullptr;
        rhs.m_Pointer = nullptr;
    }
    return *this;
}

shared_ptr is for sharing

Use a shared_ptr to express that the ownership of the resource is shared.

Reference Counting and cycles

If you have a cycle in the references between resources in std::shared_ptr, these resources will never be freed.

How to handle cyclic references?

  • manually by breaking the cycle
  • use weak references

std::weak_ptr

Weak references point to a resource, but do not prolong its lifetime.

std::weak_ptr<int> w;
{
    std::shared_ptr<int> s = std::make_shared<int>(42);
    w = s;
}
if (auto pointer = w.lock()) {
    std::cout << *pointer << std::endl;
} else {
    std::cout << "It's gone" << std::endl;
}
template <typename T>
class weak_ptr {
    // ...
    shared_ptr lock() {
        if (<It is still alive>) {
            return shared_ptr<T>(<for It>);
        }
        return shared_ptr();
    }
};

So how does shared_ptr work?

Where is the reference count stored?

  • as a member in the shared_ptr?
  • as a static member in shared_ptr<T>?

As a member?

std::shared_ptr<int> q;
{
    std::shared_ptr<int> p = std::make_shared<int>(42);
    q = p;
    // p gets destroyed here, but it can't reach within q
    // unless an expensive list of shared_ptr is stored
    // somewhere
}

As a static member?

std::shared_ptr<int> p = std::make_shared<int>(42);
std::shared_ptr<int> q = std::make_shared<int>(42);
// oops, p and q have reference count 2, but actually point to
// different resources

In the heap as a shared object

Shared Ptr

shared_ptr price

  • double indirection
    • can be optimized with a little higher memory usage
  • extra memory allocation
    • worse - it is a small memory allocation, which are most wasteful

Use std::make_shared

  • allocates a single block for object and reference count
    • the object is inplace constructed in the block
    • better cache locality

Make Shared

Intrusive reference counting

The reference count is stored in the resource (object) itself.

Intrusive

boost::intrusive_ptr

  • add a reference - calls intrusive_ptr_add_ref(T*)

  • release a reference - calls intrusive_ptr_release(T*)

    class Renderer;

    void intrusive_ptr_add_ref(Renderer* r) { ++r->refs; }

    void intrusive_ptr_release(Renderer* r) { if (--r->refs == 0) { delete r; } }

When to use intrusive_ptr?

  • some external APIs are already reference counted
    • OS, other languages
  • more memory efficient than shared_ptr

std???::intrusive_ptr

There is no std::intrusive_ptr class, because std::make_shared gives you almost the same thing.

Almost?

Using an intrusive_ptr allows you to temporary give up using smart pointers, go down to C pointer and then go up to an intrusive_ptr

std::enable_shared_from_this<T>

  • allows creating a shared_ptr from this.
  • requires that there is at least one shared_ptr pointing to the object
  • works by having a std::weak_ptr inside the object for the object itself
class Renderer : public std::enable_shared_from_this<Renderer>
{
};

std::shared_ptr<Renderer> get_the_shared_ptr(Renderer* r)
{
    return r->shared_from_this();
}

No plain new

  • std::make_shared<T> is the best way to create a shared pointer
  • C++14 - std::make_unique<T> - creates an unique pointer

Other smart pointers