-
Notifications
You must be signed in to change notification settings - Fork 35
Reference Counting
Fleece includes some utility classes for reference-counting objects. For various reasons we decided [back in 2016] to roll our own rather than use std::shared_ptr. We use an "intrusive" ref-count, meaning the ref-count is a field of the object being tracked, rather than putting ref-counts in external data structures the way shared_ptr does.
Any class to be ref-counted must inherit from fleece::RefCounted, which provides the refCount field and the retain/release logic. It has almost no visible API. Its destructor is virtual so that its _release method can delete the object correctly, calling subclass destructors.
Important rules:
- RefCounted objects must only be heap-allocated. The preferred way is to call
make_retained<>(q.v.) - RefCounted objects must never be explicitly deleted. For this reason the destructor is protected, and overrides should be protected too.
It's common to pass pointers to RefCounted objects as function parameters. Any function taking such a parameter can assume that the caller has a secure reference to the object, which will remain valid until it returns. (There are some edge cases where this can break down, e.g. if the callee calls something that triggers the caller to release its reference, but they're uncommon; the workaround is for the caller to use a Ref<> to keep the object alive for the duration of the call.)
There are standalone functions retain() and release() but you shouldn't call them unless you have a special use case and really know what you're doing. Instead you should use...
These are typical smart-pointer templates, similar to std::shared_ptr, that hold a strong reference to a RefCounted object; that is, the object is retained when assigned to a Ref<> or Retained<> variable, and it's released if it's replaced by another reference or when the Ref<> destructs.
The only difference between them is that Retained is nullable: it can hold nullptr but Ref can't. There are compile-time and runtime checks to prevent a Ref from storing null; most of the runtime checks depend on the Clang Undefined Behavior Sanitizer so it's very strongly recommended that use use that in debug builds.
We'll refer to them both as "Ref" from now on.
Refis a lot newer thanRetained, dating from late 2025, and as of this writing there's still a bunch of code that usesRetainedfor non-nullable references when it should useRef.
The preferred way to instantiate a RefCounted subclass is to use the function make_retained, which returns a Ref:
class Foo : public Retained { ... };
Ref<Foo> myFoo = make_retained<Foo>(1234);
Retained<Foo> myFoo = make_retained<Foo>(1234); // this is OK tooIn the codebase you may see this older idiom too, which is equivalent:
Retained<Foo> myFoo = new Foo(1234);This is semi-deprecated because it exposes the existence of the 'naked' object in its newborn state where it has no references yet. It also prevents mistakes like this:
auto myFoo = new Foo(1234); // ☠️ wrong!The above creates a raw Foo* that points to an object with a ref-count of zero. This won't cause problems immediately ... but when something retains and then releases that object, the release will delete it (because its ref-count dropped back to zero.)
Unlike shared_ptr, Refs implicitly convert to a raw pointer without needing to call get(), in keeping with our custom of passing references to functions as plain pointers:
void useFoo(Foo* foo);
Ref<Foo> myFooRef = ... ;
useFoo(myFooRef);However, for safety reasons a Ref will not implicitly convert to a pointer if it's an rvalue, that is, an anonymous value returned by a function. That's to prevent mistakes like this:
Ref<Foo> returnsFoo();
Foo* foo = returnsFoo(); // ☠️ wrong and illegalIf this compiled, it would result in the variable foo holding a pointer without a strong reference, which might point to a deleted object. The temporary Ref<Foo> object returned by the function would destruct at the end of that line, releasing the object and leaving a dangling pointer.
Unfortunately that means you can't do the following, which is safe; C++ doesn't have fine enough control over lifetimes to make the below legal while keeping the last example an error.
useFoo(returnsFoo()); // 🙁 won't compile
The workaround is useFoo(returnsFoo().get()).
It's fine to assign a Ref to a Retained, or pass a Ref to a parameter of type Retained. However, the other direction is less safe because it might try to assign nullptr to a Ref.
If you have a Retained that you know is non-null, you can call asRef() on it to coerce it to a Ref. This method will fail at runtime if the value is null.
The internal retain/release operations are done with atomic instructions so they're thread-safe: it's OK if a RefCounted object gets retained and released concurrently on different threads.
However, the Ref / Retained template is not thread-safe; that means it's unsafe to mutate it (change its value) on one thread while the value might be concurrently read or mutated on another thread. In most cases this isn't an issue, but if it is, you'll need to use a mutex or some equivalent to prevent concurrent access.
A single retain or release doesn't take long, or use much code, but they're ubiquitous (in LiteCore at least) so they add up. Here are some tips:
- If instances of a class will only have one owner, use
std::unique_ptrinstead ofRefCounted. - We consider it OK to pass plain pointers to RefCounted objects, as long as there aren't circumstances that could cause the strong ref you're passing to be invalidated during the call.
- Use
std::movewhen possible when copying aRef; it avoids a retain/release.