Reputation: 6636
The boost::intrusive_ptr
(or a home-made version) at its simplest looks like this:
template<typename T>
class intrusive_ptr {
public:
intrusive_ptr(T* ptr) : ptr_(ptr)
{
if (ptr_) {
intrusive_ptr_add_ref(ptr_);
}
}
intrusive_ptr(const intrusive_ptr& that) : ptr_(that.ptr_)
{
if (ptr_) {
intrusive_ptr_add_ref(ptr_);
}
}
~intrusive_ptr()
{
if (ptr_) {
intrusive_ptr_release(ptr_);
}
}
// ...
private:
ptr_;
};
Usage:
class Foo {
public:
// ...
private:
std::size_t refcount_;
friend void intrusive_ptr_add_ref(const Foo* p)
{
++p->refcount_;
}
friend void intrusive_ptr_release(const Foo* p)
{
if (--p->refcount_ == 0) { // line 1
delete p; // line 2
}
}
};
intrusive_ptr<Foo> p(new Foo);
Apparently as Foo
is implemented now, intrusive_ptr<Foo>
s aren't thread-safe. Simply changing the type of Foo::refcount_
to std::atomic<std::size_t>
won't suffice either, because when one thread is between line 1 and line 2, another thread may try to increase the reference count.
So my question is: Is it possible to make intrusive_ptr
thread-safe, ideally without resorting to heavy mechanisms like mutexes?
Upvotes: 3
Views: 1729
Reputation: 69902
You could choose to detect pointer-theft:
#include <cstdint>
#include <atomic>
#include <cassert>
#include <stdexcept>
struct allow_zero_access {};
template<typename T>
class intrusive_ptr {
public:
intrusive_ptr(T* ptr, allow_zero_access)
: ptr_(ptr)
{
assert(ptr);
intrusive_ptr_init_ref(ptr_, allow_zero_access());
}
intrusive_ptr(T* ptr) : ptr_(ptr)
{
if (ptr_) {
intrusive_ptr_add_ref(ptr_);
}
}
intrusive_ptr(const intrusive_ptr& that) : ptr_(that.ptr_)
{
if (ptr_) {
intrusive_ptr_add_ref(ptr_);
}
}
intrusive_ptr& operator=(const intrusive_ptr& that)
{
intrusive_ptr tmp(that);
std::swap(this->ptr_, tmp.ptr_);
return *this;
}
~intrusive_ptr()
{
if (ptr_) {
intrusive_ptr_release(ptr_);
}
}
// ...
private:
T* ptr_;
};
template<class T>
struct enable_mt_intrusive_pointer
{
private:
friend void intrusive_ptr_init_ref(const enable_mt_intrusive_pointer* p, allow_zero_access)
{
assert(p);
if (p->_refcount.fetch_add(1) != 0) {
throw std::logic_error("stealing someone's pointer!");
}
}
friend void intrusive_ptr_add_ref(const enable_mt_intrusive_pointer* p, bool first_access = false)
{
assert(p);
if (p->_refcount.fetch_add(1) == 0 && !first_access) {
throw std::logic_error("resurrecting a zombie");
}
}
friend void intrusive_ptr_release(const enable_mt_intrusive_pointer* p)
{
assert(p);
switch(p->_refcount.fetch_sub(1)) {
case 1:
delete p;
break;
case 0:
throw std::logic_error("already deleted");
break;
default:
;
}
}
mutable std::atomic<std::size_t> _refcount { 0 };
};
template<class T, class...Args>
intrusive_ptr<T> make_intrusive_ptr(Args&&...args)
{
return { new T(std::forward<Args>(args)...),
allow_zero_access() };
}
class Foo : public enable_mt_intrusive_pointer<Foo>
{
public:
// ...
};
int main()
{
auto p = make_intrusive_ptr<Foo>();
}
However, in practice there is rarely a reason to use intrusive_ptr's in a c++ program. Even when interfacing with external c libraries, the otherwise intrusive pointers can be wrapped in a std::shared_ptr with a custom deleter.
Upvotes: 2
Reputation: 73171
So my question is: Is it possible to make intrusive_ptr thread-safe, ideally without resorting to heavy mechanisms like mutexes?
Yes. Changing the counter to std::atomic will suffice, because if thread A reduces the counter value to zero, then it is guaranteed that no other intrusive_ptr<> objects are pointing to object p. (Because if any did exist, the refcount value would still be greater than zero).
So the race condition you're worrying about can't happen. (Well, it could happen if some other thread was dereferencing a raw pointer to object p, rather than holding an intrusive_ptr, but in that case all bets are off anyway because the program is buggy)
Upvotes: 1