Compare commits

...

1 Commits

Author SHA1 Message Date
c3915cc8af README.md WIP 2026-02-24 20:25:31 -05:00

298
README.md
View File

@ -1,3 +1,299 @@
# rcp
`rcp` is a reference-counting pointer implementation for C++.
`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`.
```cpp
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:
```cpp
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.
```cpp
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:
```cpp
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.
```cpp
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.
```cpp
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:
```cpp
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:
```cpp
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:
```cpp
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
```cpp
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:
```cpp
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.
```cpp
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:
```cpp
// 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:
```cpp
// 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.