2026-02-24 20:25:31 -05:00
2026-02-24 19:53:59 -05:00
2026-02-23 09:33:09 -05:00
2026-02-24 20:25:31 -05:00
2026-02-24 19:53:59 -05:00

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.

Description
Reference-counting pointer implementation for C++
Readme MIT 116 KiB
Languages
C++ 95.7%
Makefile 4.3%