rcp
rcp is an intrusive reference-counting smart pointer for C++11(+).
The reference count is stored inside the managed object itself, eliminating
the separate heap allocation that non-intrusive implementations require.
Memory is always freed when the last rcp to an object is destroyed.
Usage
Add rcp_managed_root to the root of your class hierarchy.
For derived classes, add rcp_managed.
class Animal
{
rcp_managed_root(Animal);
protected:
Animal(std::string name) : name(std::move(name)) {}
virtual ~Animal() {}
public:
std::string name;
};
class Dog : public Animal
{
rcp_managed(Dog);
protected:
Dog(std::string name) : Animal(std::move(name)) {}
public:
void bark() { std::cout << "Woof!\n"; }
};
rcp_managed_root injects the reference count and the increment/decrement
methods.
rcp_managed injects create() and get_rcp() into derived classes.
Both macros end with a private: access specifier, so follow them with
protected: or public: as needed.
Note: If the class will be inherited, its destructor must be marked
virtual. Without this, destroying a derived object through a base class pointer will not call the derived destructor, leading to incomplete cleanup.
Creating objects
Use the static create() method, which returns an rcp holding the newly
constructed object:
rcp<Animal> animal = Animal::create("Cat");
rcp<Dog> dog = Dog::create("Rex");
create() forwards its arguments to the class constructor.
Constructors should be protected or private to ensure objects are
always managed by rcp.
Wrapping external classes
rcp can add intrusive reference counting to a class you don't own and
cannot modify.
Derive from the external class, add rcp_managed_root to the derived
class, and give the derived class the same name inside your own namespace.
Consumers use your type and never interact with the external class
directly.
namespace external
{
class Counter
{
public:
int start;
int count;
Counter(int start) : start(start), count(start) {}
virtual ~Counter() {}
};
}
class Counter : public external::Counter
{
rcp_managed_root(Counter);
protected:
Counter(int start) : external::Counter(start) {}
~Counter() {}
};
auto a = Counter::create(10);
auto b = Counter::create(20);
a->count++; // direct access to external::Counter members
The derived class inherits all members of the external class.
create() forwards its arguments through to the external class
constructor.
The virtual destructor on the external class ensures that the derived
destructor is called correctly when the last rcp is released.
Basic usage
rcp behaves like a raw pointer for access:
rcp<Dog> dog = Dog::create("Rex");
dog->bark();
std::cout << (*dog).name << "\n";
if (dog)
std::cout << "dog is valid\n";
dog.reset(); // releases the reference; object freed if this was the last holder
Transparent handles
Defining a typedef (or using) of rcp<XImpl> as X lets consumers
use X as if it were a plain value type, with no awareness of reference
counting or implementation classes.
class ImageImpl
{
rcp_managed_root(ImageImpl);
protected:
ImageImpl(int width, int height) : width(width), height(height),
pixels(width * height) {}
~ImageImpl() {}
public:
int width, height;
std::vector<uint32_t> pixels;
};
typedef rcp<ImageImpl> Image;
Consumers work entirely with Image — creating, copying, and passing it
like a value, while the pixel data is shared automatically and freed when
no longer referenced.
Image load_thumbnail(Image full) { return full; } // shares pixel data
Image icon = Image::create(16, 16);
Image copy = icon; // shared ownership, no pixel copy
Image thumb = load_thumbnail(icon); // still the same pixel data
icon.reset();
copy.reset();
// pixel data freed here, when thumb (the last holder) goes out of scope
Copying and moving
Copying an rcp increments the reference count.
Moving transfers ownership without touching the reference count, and leaves
the source null:
rcp<Dog> a = Dog::create("Rex");
rcp<Dog> b = a; // a and b share ownership; refcount = 2
rcp<Dog> c = std::move(a); // c takes ownership from a; a is now null; refcount = 2
Upcasting
Assigning an rcp<Derived> to an rcp<Base> is implicit.
Attempting a downcast this way is a compile error:
rcp<Dog> dog = Dog::create("Rex");
rcp<Animal> animal = dog; // ok: implicit upcast
rcp<Dog> dog2 = animal; // error: implicit downcast not allowed
Downcasting
Use rcp_dynamic_cast for explicit checked downcasts.
It returns a null rcp if the object is not of the target type.
Passing an rvalue transfers ownership on success and leaves the source
unchanged on failure:
rcp<Animal> animal = Dog::create("Rex");
// copy downcast: animal remains valid regardless of outcome
rcp<Dog> dog = rcp_dynamic_cast<Dog>(animal);
// move downcast: animal is nulled on success, left intact on failure
rcp<Dog> dog2 = rcp_dynamic_cast<Dog>(std::move(animal));
Comparison
rcp<Dog> a = Dog::create("Rex");
rcp<Animal> b = a;
rcp<Dog> c = Dog::create("Buddy");
assert(a == b); // same object, different pointer types: ok
assert(a != c);
assert(c != nullptr);
Getting an rcp from this
Use get_rcp() inside a member function to obtain a reference-counted
pointer to the current object:
class Dog : public Animal
{
rcp_managed(Dog);
public:
rcp<Dog> self() { return get_rcp(); }
};
A managed class can also pass its raw this pointer directly to any
function or class that accepts an rcp, and the existing reference count
will be used — no separate control block is created.
This is safe because the reference count is stored in the object itself.
With std::shared_ptr, constructing from a raw this pointer creates an
independent control block, leading to a double-free when both reach zero.
struct EventListener;
static std::vector<rcp<EventListener>> g_listeners;
struct EventListener
{
rcp_managed_root(EventListener);
public:
int events_received = 0;
void attach() { g_listeners.push_back(this); }
};
auto a = EventListener::create();
auto b = EventListener::create();
a->attach();
b->attach();
for (auto & l : g_listeners) l->events_received++;
Comparison with std::shared_ptr
rcp |
std::shared_ptr |
|
|---|---|---|
| Reference count location | Inside the object | Separate control block (extra allocation) |
| Class requirement | rcp_managed_root / rcp_managed macro |
None (any type) |
| Weak pointers | No | Yes (std::weak_ptr) |
| Custom deleters | No | Yes |
Pointer to this |
get_rcp() |
std::enable_shared_from_this |
| Checked downcast | rcp_dynamic_cast |
std::dynamic_pointer_cast |
The intrusive design means each managed object requires only one heap
allocation instead of two.
The trade-off is that the class must be written with rcp in mind.
std::enable_shared_from_this is tied to the type it is inherited from,
so shared_from_this() always returns a shared_ptr to that base type.
A derived class method that needs a shared_ptr to its own type must
manually cast:
// shared_ptr: verbose and requires knowing the base
std::shared_ptr<Dog> self()
{
return std::dynamic_pointer_cast<Dog>(shared_from_this());
}
With rcp, rcp_managed injects a get_rcp() that returns the correct
derived type directly:
// rcp: returns rcp<Dog> directly, no cast needed
rcp<Dog> self() { return get_rcp(); }
Comparison with boost::intrusive_ptr
Both are intrusive reference-counting pointers, but they differ in how the reference counting is wired up:
rcp |
boost::intrusive_ptr |
|
|---|---|---|
| Setup | rcp_managed_root / rcp_managed macros |
User-defined intrusive_ptr_add_ref / intrusive_ptr_release free functions and ref-count storage |
| Upcasting | Implicit via constructor | Implicit via raw pointer conversion |
| Checked downcast | rcp_dynamic_cast |
Not built in |
rcp automates the boilerplate of wiring up the reference count.
boost::intrusive_ptr is more flexible — it can wrap types that already
have their own reference counting (e.g. COM objects, OS handles) by
implementing the free functions to call the existing mechanism.