From af0e0d00840e9b268124a9341cedc077b3d85fe8 Mon Sep 17 00:00:00 2001 From: Josh Holtrop Date: Tue, 24 Feb 2026 19:54:15 -0500 Subject: [PATCH] Add README contents --- README.md | 367 +++++++++++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 366 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 16d13b5..d0f786c 100644 --- a/README.md +++ b/README.md @@ -1,3 +1,368 @@ # rcp -`rcp` is a reference-counting pointer implementation for C++. +`rcp` is an intrusive reference-counting smart pointer for C++11(+). +In contrast to smart pointer implementations that use a separately allocated +control block with reference count (e.g. `std::shared_ptr`), the reference +count is stored inside the managed object itself, eliminating the separate heap +allocation that non-intrusive implementations require. +The managed object is automatically destructed when the last `rcp` pointing to +the object is destroyed. + +Importantly, since the reference count is stored within the managed object, +this gives the object the freedom to pass its `this` pointer to any other +object or function which takes a `rcp` managed pointer and a new `rcp` managed +pointer can be automatically created from `this` to point to the same reference +count. +This is in contrast to `std::shared_ptr` or other smart pointer implementations +which store the reference count externally to the managed object, where +constructing another managed pointer from a raw `this` pointer can lead to a +double free situation with two managed pointers pointing at the same object. + +The tradeoff is that the class definition must be modified to declare support +for being managed by an `rcp` smart pointer. +This does not mean, however, that class definitions which cannot be modified +cannot be managed by `rcp` smart pointers. +See the [Wrapping External Classes](#wrapping-external-classes) section for an +explanation of how to do this. + +`rcp` is MIT-licensed. +See [https://github.com/holtrop/rcp](https://github.com/holtrop/rcp) for issues, releases, and pull requests. + +## Installation + +No installation is required to use `rcp`. +`rcp` is entirely implemented within one header file `rcp.h`. +Simply copy the header file (`rcp.h`) into your project or add the `rcp` +repository as a submodule and add the `include` directory to your build system +include path. + +## Basic Usage + +`rcp` behaves like a raw pointer for access: + +```cpp +rcp dog = Dog::create("Rex"); +dog->bark(); +std::cout << (*dog).name << "\n"; + +if (dog) + std::cout << "dog is valid\n"; + +// When `dog` goes out of scope and its destructor is called, it will decrement +// the reference count of the `Dog` instance. If this was the last reference +// to the object, the `Dog` instance will be deleted. Calling `reset()` on an +// instance of a `rcp` managed pointer will also decrement the reference count +// of the object currently being pointed to and then reset the reference to a +// null reference. +dog.reset(); +``` + +## Defining Classes With rcp Support + +Example: + +```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"; } +}; +``` + +Add a call to the `rcp_managed_root` macro to the root of your class hierarchy. +The argument to the macro is the name of the class. +In a class hierarchy, there should be only one `rcp_managed_root` call at the +highest managed level. +Use `rcp_managed` for any classes deriving from a class using +`rcp_managed_root`. +`rcp_managed_root` injects the reference count and the increment/decrement +methods. +It also calls `rcp_managed` internally. +`rcp_managed` injects `create()` and `get_rcp()` into derived classes. + +In this way, there will be only one reference count variable and one set of +increment/decrement functions defined in any object. +In contrast, the `get_rcp()` function will be defined within each class in the +hierarchy so that it returns a reference to the specific type that it is +called on. + +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 + +The managed object's constructor should generally be declared with `protected` +visibility instead of `public` visibility. +This prevents a user from constructing an object instance without a managed +pointer to it. +The `rcp_managed` macro defines a public `create()` static function which will +forward all arguments to the constructor and then call `get_rcp()` on the +resulting object to create the initial managed pointer to it. + +In addition to the class itself defining a `create()` function, the `rcp` +managed pointer class also declares a `create()` function which can be used to +create an instance of the managed object. +See [Transparent Handles](#transparent-handles) for a way this can be used. + +Users use the static `create()` method, which returns an `rcp` holding a +managed pointer to the newly constructed object: + +```cpp +rcp animal = Animal::create("Cat"); +rcp dog = Dog::create("Rex"); +``` + +## Wrapping External Classes + +`rcp` can add intrusive reference counting to a class you don't own and +cannot modify. +Derive from the external class and add `rcp_managed_root` to the derived +class. +You can give the derived class the same name inside your own namespace or a +different name if desired. +Consumers use your type and never interact with the external class directly. + +Example: + +```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. + +## Transparent Handles + +Defining a `typedef` (or `using`) of `rcp` as `X` lets consumers +use `X` as if it were a plain value type, with no awareness of reference +counting or implementation classes. + +Example: + +```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 pixels; +}; +typedef rcp 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 +``` + +This usage pattern is supported by `rcp` but is completely optional. +This usage pattern provides a more concise syntax, however it could be less +obvious to the user what is happening under the hood so it is entirely up to +the user whether or not to use this pattern. + +## Copying and Moving + +Copying an `rcp` increments the reference count. +Moving efficiently transfers ownership without touching the reference count, +and leaves the source null: + +```cpp +rcp a = Dog::create("Rex"); +rcp b = a; // a and b share ownership; refcount = 2 +rcp c = std::move(a); // c takes ownership from a; a is now null; refcount = 2 +``` + +## Upcasting + +Assigning an `rcp` to an `rcp` is implicit. +Attempting a downcast this way is a compile error: + +```cpp +rcp dog = Dog::create("Rex"); +rcp animal = dog; // ok: implicit upcast +rcp 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 = Dog::create("Rex"); + +// copy downcast: animal remains valid regardless of outcome +rcp dog = rcp_dynamic_cast(animal); + +// move downcast: animal is nulled on success, left intact on failure +rcp dog2 = rcp_dynamic_cast(std::move(animal)); +``` + +## Comparison + +```cpp +rcp a = Dog::create("Rex"); +rcp b = a; +rcp 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 self() { return get_rcp(); } +}; +``` + +A managed class can also pass its raw `this` pointer directly to any +function or class method 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`, in contrast, constructing from a raw `this` pointer +creates an independent control block, leading to a double-free when both reach +zero. + +Example: + +```cpp +struct EventListener; +static std::vector> 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` | implicit conversion from `this` or explicit `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 which can improve performance for many applications. +The trade-off is that the class must be written with `rcp` in mind. + +Note that `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 self() +{ + return std::dynamic_pointer_cast(shared_from_this()); +} +``` + +With `rcp`, `rcp_managed` injects a `get_rcp()` that returns the correct +derived type directly: + +```cpp +// rcp: returns rcp directly, no cast needed +rcp 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` can be more flexible for some uses cases because 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.