Announcing rcp 1.0.0: A Refined Reference Counting Pointer for C++

Managing object lifecycles in C++ has always been a balance between safety, performance, and ergonomics. While the Standard Library gives us std::shared_ptr and Boost offers boost::intrusive_ptr, there are often cases where neither is the “perfect” fit.

Today, I’m excited to announce the 1.0.0 release of rcp, a C++ library designed to provide a lightweight, intrusive reference-counting pointer that prioritizes simplicity and performance.

What is rcp?

rcp (short for Reference Counted Pointer) is a header-only C++ library. It provides an intrusive reference counting mechanism, meaning the reference count is stored within the object itself rather than in an external control block.

You can find the project here: https://github.com/holtrop/rcp

Key Features

  • Intrusive Design: By storing the reference count inside the object, we eliminate the need for a separate memory allocation for a control block (unlike std::shared_ptr).
  • Minimal Overhead: Since the pointer itself is essentially just a raw pointer, it is incredibly lightweight.
  • Flexibility: It supports custom base classes and can be integrated into existing class hierarchies with ease.
  • Thread Safety: Designed to handle reference count increments and decrements safely in multi-threaded environments.
  • Zero Dependencies: It’s a single-header library—just drop it in and go.

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

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.


How does it compare?

When choosing a smart pointer, it’s important to understand the trade-offs. Here is how rcp stacks up against the most common alternatives:

vs std::shared_ptr

  • Memory Efficiency: std::shared_ptr requires a “control block” to store the reference count and deleter. If you don’t use std::make_shared, this results in two allocations. rcp stores the count in the object, requiring only one allocation.
  • Pointer Size: A std::shared_ptr is typically two pointers in size (object pointer + control block pointer). rcp::ptr is the size of a single raw pointer.
  • Flexibility: You can create an rcp::ptr from a raw pointer (this) safely at any time if the object inherits from the base class, without the complexity of std::enable_shared_from_this.

vs boost::intrusive_ptr

  • Modernity: rcp is designed with modern C++ patterns in mind, offering a cleaner API and better integration with C++11/14/17+ features.
  • Ease of Use: While boost::intrusive_ptr requires you to manually define intrusive_ptr_add_ref and intrusive_ptr_release in the namespace of your class, rcp provides a macro to handle the boilerplate for you.
  • Lightweight: rcp aims to be even more minimal than the Boost implementation, making it ideal for projects where pulling in a large dependency like Boost is not feasible.

Getting Started

The 1.0.0 release marks a milestone of stability for the API. Whether you are working on a game engine, a high-performance networking tool, or an embedded system, rcp offers a robust way to manage memory without the bloat.

Check out the GitHub repository for the full documentation, and feel free to open an issue or a pull request!

Comments are closed.