Took a shot at implementing std::shared_ptr
, with a thread-safe refcount and weak count. Didn't do weak_ptr
, I'm doing this for learning purposes, and I think from an educational standpoint there's not much to learn from adding it.
Also tried to implement the "we know where you live" optimization, with the control block and the underlying memory allocated together in makeShared
, but it feels sloppy and error prone and would love advice on if it can be done more cleanly.
#include <atomic>
#include <iostream>
template <typename T>
class ControlBlock {
public:
ControlBlock():
m_ptr{nullptr},
m_ref_count{0},
m_weak_count{0}
{}
ControlBlock(T* ptr):
m_ptr{ptr},
m_ref_count{1},
m_weak_count{0}
{}
void add_reference() {
m_ref_count.fetch_add(1, std::memory_order_relaxed);
}
void add_weak_reference() {
m_weak_count.fetch_add(1, std::memory_order_relaxed);
}
void remove_reference() {
// The release memory barrier ensures all operations on this thread are
// visible to other threads before a decrement happens
if (m_ref_count.fetch_sub(1, std::memory_order_release) == 1) {
// the acquire memory barrier makes sure we delete the pointer
// and control block only after the ref count has gone to 0
std::atomic_thread_fence(std::memory_order_acquire);
m_ptr->~T();
if (m_weak_count.load(std::memory_order_relaxed) == 0) {
delete this;
}
}
}
void remove_weak_reference() {
if (m_weak_count.fetch_sub(1, std::memory_order_release) == 1) {
std::atomic_thread_fence(std::memory_order_acquire);
if (m_ref_count.load(std::memory_order_relaxed) == 0) {
delete this;
}
}
}
T* get() const noexcept {
return m_ptr;
}
std::size_t use_count() const noexcept {
return m_ref_count.load(std::memory_order_relaxed);
}
std::size_t weak_count() const noexcept {
return m_weak_count.load(std::memory_order_relaxed);
}
template<typename... Args>
static ControlBlock* create (Args&&... args) {
std::size_t cb_size = sizeof(ControlBlock);
std::size_t t_size = sizeof(T);
std::size_t t_alignment = alignof(T);
std::size_t remainder = cb_size % t_alignment;
std::size_t padding = remainder ? (t_alignment - remainder) : 0;
std::size_t t_offset = cb_size + padding;
char* chunk = static_cast<char*>(::operator new(cb_size + t_size));
T* ptr = nullptr;
try {
ptr = new (chunk + t_offset) T(std::forward<Args>(args)...);
} catch (...) {
::operator delete (chunk);
throw;
}
try {
return new(chunk) ControlBlock{ptr};
} catch(...) {
ptr->~T();
::operator delete(chunk);
throw;
}
}
private:
T* m_ptr;
std::atomic<std::size_t> m_ref_count;
std::atomic<std::size_t> m_weak_count;
};
template<typename T>
class SharedPtr {
public:
SharedPtr() noexcept : cb{new ControlBlock<T>()} {}
SharedPtr(T* item) : cb{new ControlBlock<T>{item}} {}
template<typename U>
requires(std::is_convertible_v<U*, T*>)
SharedPtr(SharedPtr<U>&& other) noexcept
: SharedPtr{other.cb}
{
other.cb = nullptr;
}
SharedPtr(const SharedPtr& other) : cb{other.cb} {
cb->add_reference();
}
SharedPtr (SharedPtr&& other) : cb{other.cb} {
other.cb = nullptr;
}
SharedPtr& operator=(const SharedPtr& other) {
if (this == &other) return *this;
SharedPtr temp{other};
swap(temp);
return *this;
}
SharedPtr& operator=(SharedPtr&& other) {
if (this == &other) return *this;
SharedPtr temp{std::move(other)};
swap(temp);
return *this;
}
int use_count() const noexcept {
return cb -> use_count();
}
void swap(SharedPtr& other) noexcept {
std::swap(cb, other.cb);
}
T* reset() {
T* result = get();
SharedPtr new_ptr;
swap(new_ptr);
return result;
}
T* reset(T* new_val) {
T* result = get();
SharedPtr new_ptr{new_val};
swap(new_ptr);
return result;
}
T* get() const noexcept {
return cb->get();
}
T& operator*() const {
return *get();
}
T* operator->() const noexcept {
return get();
}
explicit operator bool() const noexcept {
return get() != nullptr;
}
~SharedPtr() {
if (cb) {
cb -> remove_reference();
}
}
private:
ControlBlock<T>* cb;
// Make all specializations of SharedPtr friends
// with one another
template<typename U>
friend class SharedPtr;
// Make makeShared a friend of the SharedPtr class
// so we can use the private ControlBlock constructor
template<typename U, typename...Args>
friend SharedPtr<U> makeShared(Args&&... args);
SharedPtr(ControlBlock<T>* cb) : cb{cb} {}
template <typename U>
SharedPtr(ControlBlock<U>* cb) :
cb{reinterpret_cast<ControlBlock<T>*>(cb)} {}
};
template<typename T, typename... Args>
SharedPtr<T> makeShared(Args&&... args) {
auto cb = ControlBlock<T>::create(std::forward<Args>(args)...);
return SharedPtr<T>{cb};
}
Added some unit tests as well, based on the example behavior on cppreference, but again would love some advice on how to really stress test this, especially since the control block creation is so manual and error prone.
// Have to define the test module first, always.
#define BOOST_TEST_MODULE sharedptrtest
#ifdef BOOST_TEST_DYN_LINK
# include <boost/test/unit_test.hpp>
#else
# include <boost/test/included/unit_test.hpp>
#endif // BOOST_TEST_DYN_LINK
#include <boost/test/data/test_case.hpp>
#include <boost/test/data/monomorphic.hpp>
#include "SharedPtr.h"
struct Foo {
int id{0};
Foo (int i = 0) : id{i} {}
~Foo() = default;
int val() {
return id;
}
};
struct B
{
virtual ~B() = default;
virtual int val() const {return 0;}
};
struct D : B
{
D() { }
~D() {}
int val() const override {
return 2;
}
};
BOOST_AUTO_TEST_CASE(default_constructor_test)
{
{
SharedPtr<Foo> s1;
}
}
BOOST_AUTO_TEST_CASE(runtime_poly)
{
SharedPtr<B> p = makeShared<D>();
assert(p -> val() == 2);
}
BOOST_AUTO_TEST_CASE(copy_constructor_test)
{
SharedPtr<Foo> s1(new Foo{10});
BOOST_CHECK_EQUAL(s1.use_count(), 1);
{
SharedPtr<Foo> s2{s1};
BOOST_CHECK_EQUAL(s1.use_count(), 2);
}
BOOST_CHECK_EQUAL(s1.use_count(), 1);
}
BOOST_AUTO_TEST_CASE(copy_assigment_test)
{
SharedPtr<Foo> s1(new Foo{10});
BOOST_CHECK_EQUAL(s1.use_count(), 1);
{
SharedPtr<Foo> s2 = s1;
BOOST_CHECK_EQUAL(s1.use_count(), 2);
}
BOOST_CHECK_EQUAL(s1.use_count(), 1);
}
BOOST_AUTO_TEST_CASE(move_assigment_test)
{
SharedPtr<Foo> s1(new Foo{10});
BOOST_CHECK_EQUAL(s1.use_count(), 1);
SharedPtr<Foo> s2 = std::move(s1);
BOOST_CHECK_EQUAL(s2.use_count(), 1);
}
BOOST_AUTO_TEST_CASE(reset_test)
{
SharedPtr<Foo> s1(new Foo{10});
BOOST_CHECK_EQUAL(s1->val(), 10);
BOOST_CHECK_EQUAL(s1.use_count(), 1);
s1.reset(new Foo{20});
BOOST_CHECK_EQUAL(s1->val(), 20);
BOOST_CHECK_EQUAL(s1.use_count(), 1);
s1.reset();
BOOST_CHECK_EQUAL(s1.use_count(), 0);
}
BOOST_AUTO_TEST_CASE(swap_test)
{
SharedPtr<Foo> s1 = makeShared<Foo>(100);
SharedPtr<Foo> s2 = makeShared<Foo>(200);
BOOST_CHECK_EQUAL(s1->val(), 100);
BOOST_CHECK_EQUAL(s2->val(), 200);
BOOST_CHECK_EQUAL(s1.use_count(), 1);
BOOST_CHECK_EQUAL(s1.use_count(), 1);
s1.swap(s2);
BOOST_CHECK_EQUAL(s1->val(), 200);
BOOST_CHECK_EQUAL(s2->val(), 100);
BOOST_CHECK_EQUAL(s1.use_count(), 1);
BOOST_CHECK_EQUAL(s1.use_count(), 1);
}
BOOST_AUTO_TEST_CASE(get_and_deref_test)
{
int* pInt = new int{42};
SharedPtr shInt = makeShared<int>(42);
BOOST_CHECK_EQUAL(*pInt, *shInt);
BOOST_CHECK_EQUAL(*pInt, *(shInt.get()));
}
BOOST_AUTO_TEST_CASE(bool_test)
{
SharedPtr<int> ptr;
BOOST_CHECK(!(static_cast<bool>(ptr)));
ptr = makeShared<int>(7);
BOOST_CHECK((static_cast<bool>(ptr)));
}
```