Gcpp
Experimental deferred and unordered destruction library for C++
Install / Use
/learn @hsutter/GcppREADME
You can find a talk that describes this library here:
gcpp: Deferred and unordered destruction
Herb Sutter -- Updated 2016-10-16
Motivation, goals, and disclaimers
gcpp is a personal project to try an experiment: Can we take the deferred and unordered destruction patterns with custom reachability tracing logic that we find ourselves writing by hand today, and automate some parts of that work as a reusable C++ library that delivers it as a zero-overhead abstraction?
This is a demo of a potential additional fallback option for the rare cases where unique_ptr and shared_ptr aren't quite enough, notably when you have objects that refer to each other in local owning cycles, or when you need to defer destructor execution to meet real-time deadlines or to bound destructor stack cost. The goal is to illustrate ideas that others can draw from, that you may find useful even if you never use types like the ones below but just continue to use existing smart pointers and write your destructor-deferral and tracing code by hand.
Disclaimers: This is a demo, not a production quality library; bug reports are welcome. As of this writing, it works on Clang/libc++ 3.9 or later and Visual Studio 2015 Update 3 or later; if you have success with others, please report it by opening an Issue to update this README. See also the FAQ "So deferred_heap and deferred_ptr have no disadvantages?". And please see the Acknowledgments.
Overview
deferred_heap
A deferred_heap owns a region of memory containing objects that can be safely accessed via deferred_ptr<T> and that can point to each other arbitrarily within the same heap. It provides three main operations:
-
.make<T>()allocates and constructs a newTobject and returns adeferred_ptr<T>. IfTis not trivially destructible, it will also record the destructor to be eventually invoked. -
.collect()is called explicitly by default and traces this local heap in isolation; it never traces outside this heap and its roots, including that it does not touch the memory owned by any otherdeferred_heaps or any other program data structures. Any unreachable objects will have their deferred destructors run before their memory is deallocated. Cycles ofdeferred_ptrs within the same heap are destroyed when the cycle is no longer reachable. -
~deferred_heap()runs any remaining deferred destructors and resets anydeferred_ptrs that outlive this heap to null, then releases its memory all at once like a region.
Because .collect() and the destructor are explicit, the program can choose when (at a convenient time) and where (e.g., on what thread or processor) to run destructors.
Local small heaps are encouraged. This keeps tracing isolated and composable; combining libraries that each use deferred_heaps internally will not directly affect each other's performance.
deferred_ptr<T>
A deferred_ptr<T> refers to a T object in a deferred_heap. You can construct a deferred_ptr from an existing one (including one returned by deferred_heap::make, the source of all non-null deferred_ptrs) or by default-constructing it to null.
It has the same functions as any normal C++ smart pointer, including derived-to-base and const conversions. Here are the main additional features and semantics:
-
A
deferred_ptrmember is null in a deferred destructor. This enables safe unordered destruction by enforcing known best practices long learned in other languages and environments, in particular:-
It enforces that the otherwise-normal deferred destructor cannot access another deferred-destruction object that could possibly be destroyed in the same collection cycle (since it might already have been destroyed). This eliminates the "accessing a disposed/finalized object" class of bugs that is possible in environments like Java, .NET, and Go, and eliminates the "leaking cycles of objects that use finalizers" class of bugs that is possible in Go.
-
It enforces that a destructor cannot "resurrect" another deferred-lifetime object (or itself) by storing a
deferred_ptrto it somewhere that would make that object reachable again. This eliminates the "double dispose" class of bugs that is possible in environments like Java, .NET, and Go. It also closes a second route to the "accessing a disposed/finalized object" class of bugs mentioned in the previous point.
-
-
Like
shared_ptr's aliasing constructor,deferred_ptrsupports creating a smart pointer to a data member subobject of a deferred object. However, I'm currently doing it in a different way fromshared_ptr. Whereasshared_ptr's aliasing constructor can accept any pointer value and so isn't type-safe or memory-safe,deferred_ptr<T>instead provides a.ptr_to<U>(U T::*)function that takes a pointer to member ofTand so type- and memory-safely guarantees at compile time that you can only use it to form adeferred_ptr<U>to a validUsubobject of a deferredTobject via a validdeferred_ptr<T>. -
Assigning a
deferred_ptris not allowed across heaps. The pointer must continue pointing into whichever heap it initially pointed into. A default-constructed pointer does not point into any heap, but once assigned to point into a given heap it must continue to point there; this is currently enforced in debug builds.
deferred_allocator
Finally, deferred_allocator is a C++11 STL allocator that you can use to have an STL container store its elements in a deferred_heap. This is especially useful when a deferred-lifetime object contains a container of deferred_ptrs to other objects in its heap; using deferred_allocator expresses that the container's deferred_ptrs are inside the heap so that any cycles they participate in can be automatically cleaned up, whereas not using deferred_allocator expresses that the container's deferred_ptrs are outside the heap (i.e., roots).
Convenience aliases are provided; for example, deferred_vector<T> is an alias for std::vector<T, gcpp::deferred_allocator<T>>.
See also additional uses of deferred_allocator.
Example
Here is a Graph type that has its own local heap shared by all Graph objects:
// possibly-cyclic N-ary Graph, one heap for all graph nodes
class Graph {
struct Node {
deferred_vector<deferred_ptr<Node>> outbound_edges{ my_heap }; // keeps all edges in the heap
/*... data ...*/
};
static deferred_heap my_heap;
vector<deferred_ptr<Node>> roots;
public:
void SampleFunction() {
roots.emplace_back(my_heap.make<Node>());
}
// ... etc.
};
Here is a variation where each Graph object has its own private local heap:
// possibly-cyclic N-ary Graph, one heap per graph object
class Graph {
struct Node {
/*... data ...*/
deferred_vector<deferred_ptr<Node>> outbound_edges; // keeps all edges in the heap
Node(deferred_heap& h) : outbound_edges{h} { }
};
deferred_heap my_heap;
vector<deferred_ptr<Node>> roots;
public:
void SampleFunction() {
roots.emplace_back(my_heap.make<Node>(my_heap));
}
// ... etc.
};
Note that deferred_allocator requires C++11 allocator support for fancy pointers; see deferred_allocator implementation notes.
Target use cases
gcpp aims to address three main issues, and a speculative use case.
1. Ownership cycles, preventing leaks
The first target use case is automatic by-construction memory management for data structures with cycles, which cannot be expressed with fully automated lifetime using C++17 facilities only.
-
Encapsulated example: A
Graphtype whoseNodes point to each other but should stay alive as long as they are transitively reachable from the enclosingGraphobject or from some object such as aGraph::iteratorthat is handed out by theGraphobject. -
Unencapsulated example: A group of objects of different types that refer to each other in a potentially-cyclic way but should stay alive as long as they are transitively reachable from some root outside their
deferred_heap. -
In C++17, both examples can be partly automated (e.g., having
Graphcontain avector<unique_ptr<Node>> my_nodes;so that all nodes are guaranteed to be destroyed at least when theGraphis destroyed), but require at minimum manual liveness tracing today (e.g., traversal logic to discover unreachableNodeobjects, which essentially implements a custom tracing algorithm by hand for the nodes, and thenmy_nodes.erase(unreachable_node);to remove each unreachable node which is manual and morally equivalent todelete unreachable_node;).
2. Real time systems, bounding the cost of pointer assignment
Using shared_ptr can be problematic in real time systems code, because any simple shared_ptr pointer assignment or destruction could cause an arbitrary number of objects to be destroyed, and therefore have unbounded cost. This is a rare case of where prompt destruction, usually a wonderful property, is actually bad for performance. (Note: This is less of a problem with unique_ptr because unique_ptr assignment necessarily causes destruction and so tends to be treated more carefully, whereas shared_ptr assignment might cause destruction.)
Today, deadline-driven code must either avoid manipulating shared_ptrs or
