rcp
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 section for an
explanation of how to do this.
rcp is MIT-licensed.
See 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:
rcp<Dog> 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:
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 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:
rcp<Animal> animal = Animal::create("Cat");
rcp<Dog> 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:
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<XImpl> as X lets consumers
use X as if it were a plain value type, with no awareness of reference
counting or implementation classes.
Example:
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
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:
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 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:
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 |
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:
// 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 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.