I wrote a simple lighter version of std::shared_ptr<T>
called res_ptr<T>
.
std::shared_ptr<T>
is somewhat heavy due to all the nuances of its control block: support for safe deleting without virtual destructor, support for std::weak_ptr<T>
(I am not certain on all details of the implementation... but I believe that it either requires an extra allocation for the control block, or has complicated logic on when to delete...).
The res_ptr<T>
requires T
to inherit from class resource
that manages the reference counter and deletes the instance upon destruction once the counter reaches zero requires. Yes, it doesn't support array version res_ptr<T[]>
and I don't think that it should but I might wrong.
res_version<T>
is a helper class that attaches resource
class to classes that do not inherit from resource
that shouldn't change anything besides that.
Besides that, it is supposed to have fairly same interface as std::shared_ptr<T>
and std::unique_ptr<T>
. I know it currently lacks option for custom deleter but I don't think that I'll ever need it so I don't intend to implement it.
The implementation I have is written on Visual Studio and isn't fully portable. Though, honestly, this class is going to be the least of my worries once I need portability for the codebase. Though, if it is something else besides modifying #pragma once
or issues with initialization of std::atomic
I'd like to know.
I'd like to know if there are any issues or downsides that I failed to consider as well as general code review.
#pragma once
#include <atomic>
class resource
{
public:
virtual ~resource() = default;
resource() = default;
// moving / copying does not alter the reference counter
resource(resource&&) noexcept : resource() {};
resource(const resource&) noexcept : resource() {};
resource& operator = (resource&&) noexcept {};
resource& operator = (const resource&) noexcept {};
void add_ref() const noexcept
{
m_refcount.fetch_add(1, std::memory_order_relaxed);
}
int reduce_ref() const noexcept
{
return m_refcount.fetch_sub(1, std::memory_order_relaxed)-1;
}
int count() const noexcept
{
return m_refcount.load(std::memory_order_relaxed);
}
private:
mutable std::atomic<int> m_refcount = 0;
};
template<typename T>
class res_version :
public T, public resource
{
public:
template<typename... Args>
res_version(Args... args):
T(std::forward<Args>(args)...)
{};
};
template<typename PResource, typename Enable = void>
class res_ptr;
template<typename PResource>
class res_ptr<PResource, typename std::enable_if_t<std::is_base_of_v<resource, PResource>>>
{
public:
template<typename PResourceDerived, typename E>
friend class res_ptr;
constexpr res_ptr() noexcept = default;
constexpr res_ptr(nullptr_t) noexcept {};
template<typename PResourceDerived, std::enable_if_t<std::is_base_of_v<PResource, PResourceDerived>, int> = 0>
explicit res_ptr(PResourceDerived* ptr) : m_ptr(static_cast<PResource*>(ptr))
{
if(m_ptr) m_ptr->add_ref();
};
~res_ptr()
{
release();
}
// copy ctor
res_ptr(const res_ptr& ptr) noexcept :
m_ptr(ptr.get())
{
if (m_ptr) m_ptr->add_ref();
};
// copy ctor cast
template<typename PResourceDerived, std::enable_if_t<std::is_base_of_v<PResource, PResourceDerived> && !std::is_same_v<PResource, PResourceDerived>,int> = 0>
res_ptr( const res_ptr<PResourceDerived> & ptr) noexcept :
m_ptr(static_cast<PResource*>(ptr.get()))
{
if (m_ptr) m_ptr->add_ref();
};
// move ctor
res_ptr( res_ptr&& ptr) noexcept :
m_ptr(std::exchange(ptr.m_ptr, nullptr))
{};
// move ctor cast
template<typename PResourceDerived, std::enable_if_t<std::is_base_of_v<PResource, PResourceDerived> && !std::is_same_v<PResource, PResourceDerived>,int> = 0>
res_ptr( res_ptr<PResourceDerived> && ptr) noexcept :
m_ptr(static_cast<PResource*>(std::exchange(ptr.m_ptr, nullptr)))
{};
// copy
res_ptr& operator = (const res_ptr& other) noexcept
{
if (this != &other)
{
release();
m_ptr = other.m_ptr;
if (m_ptr) m_ptr->add_ref();
}
return *this;
}
// move
res_ptr& operator = ( res_ptr&& other) noexcept
{
if (this != &other)
{
release();
m_ptr = std::exchange(other.m_ptr,nullptr);
}
return *this;
}
// copy cast
template<typename PResourceDerived, std::enable_if_t<std::is_base_of_v<PResource, PResourceDerived> && !std::is_same_v<PResource, PResourceDerived>, int> = 0>
res_ptr& operator = (const res_ptr<PResourceDerived>& other) noexcept
{
release();
m_ptr = static_cast<PResource*>(other.m_ptr);
if (m_ptr) m_ptr->add_ref();
return *this;
}
// move cast
template<typename PResourceDerived, std::enable_if_t<std::is_base_of_v<PResource, PResourceDerived> && !std::is_same_v<PResource, PResourceDerived>, int> = 0>
res_ptr& operator = ( res_ptr<PResourceDerived>&& other) noexcept
{
release();
m_ptr = static_cast<PResource*>(std::exchange(other.m_ptr,nullptr));
return *this;
}
PResource* operator -> () const noexcept
{
return m_ptr;
}
PResource& operator * () const noexcept
{
return *m_ptr;
}
PResource* get() const noexcept
{
return m_ptr;
}
operator bool () const noexcept
{
return m_ptr != nullptr;
}
void release()
{
if (m_ptr && (m_ptr->reduce_ref() == 0))
{
delete m_ptr;
}
}
template<typename PResource>
bool operator == (const res_ptr<PResource>& other) noexcept
{
return m_ptr == other.m_ptr;
}
template<typename PResource>
bool operator != (const res_ptr<PResource>& other) noexcept
{
return m_ptr != other.m_ptr;
}
private:
PResource* m_ptr = nullptr;
};
template<typename PResource, typename... Args>
res_ptr<PResource> make_resource(Args&& ... args)
{
return res_ptr<PResource>(new PResource(std::forward<Args>(args)...));
}
template<typename PResourceDerived, typename PResourceBase>
res_ptr<PResourceDerived> resource_dynamic_cast(const res_ptr<PResourceBase>& uPtr) noexcept
{
PResourceDerived* ptr = dynamic_cast<PResourceDerived*>(uPtr.get());
return res_ptr<PResourceDerived>(ptr);
}
std::shared_ptr
is not light wait. Its difficult to write correctly and needs all that code. \$\endgroup\$res_ptr<T>
is not designed to replacestd::shared_ptr<T>
but to provide a more efficient version for certain classes. \$\endgroup\$std::shared_ptr
(having read the code) you would need to show me evidence of that. Also sincestd::shared_ptr
has much more testing on it I am inclined to think there is something non obvious here that will cause problems (without lots of though going into testing and validating). They found bugs in shared_ptr for well over a decade after it was first written before it got to its current state. It is a good try (probably one of the best I have seen) but this should not be used in production code. \$\endgroup\$release
on reducing count, and additionalacquire
prior to delete. \$\endgroup\$