You are probably in need of a Garbage Collector technique such as Mark and Sweep. The idea of this algorithm is:
- Keep a list with references to all memory blocks allocated.
- At some point you start the garbage collector:
- It first marks all the blocks it can still access without using the reference list.
- It goes through the list erasing each item that could not be marked, implying it is not reachable anymore so it is not useful.
Since you are using shared_ptr
any still existing pointers you fail to reach should be considered as members of a cycle.
Implementation
Below I describe a very naive example of how to implement the sweep()
part of the algorithm, but it will reset()
all remaining pointers on the collector.
This code stores shared_ptr<Cycle_t>
pointers. The class Collector
is responsible for keeping track of all the pointers and deleting them when sweep()
is executed.
#include <vector>
#include <memory>
class Cycle_t;
typedef std::shared_ptr<Cycle_t> Ref_t;
// struct Cycle;
struct Cycle_t {
Ref_t cycle;
Cycle_t() {}
Cycle_t(Ref_t cycle) : cycle(cycle) {}
};
struct collector {
// Note this vector will grow endlessy.
// You should find a way to reuse old links
std::vector<std::weak_ptr<Cycle_t>> memory;
// Allocate a shared pointer keeping
// a weak ref on the memory vector:
inline Ref_t add(Ref_t ref) {
memory.emplace_back(ref);
return ref;
}
inline Ref_t add(Cycle_t value) {
Ref_t ref = std::make_shared<Cycle_t>(value);
return add(ref);
}
inline Ref_t add() {
Ref_t ref = std::make_shared<Cycle_t>();
return add(ref);
}
void sweep() {
// Run a sweep algorithm:
for (auto& ref : memory) {
// If the original shared_ptr still exists:
if (auto ptr = ref.lock()) {
// Reset each pointer contained within it:
ptr->cycle.reset();
// Doing this will trigger a deallocation cascade, since
// the pointer it used to reference will now lose its
// last reference and be deleted by the reference counting
// system.
//
// The `ptr` pointer will not be deletd on the cascade
// because we still have at least the current reference
// to it.
}
// When we leave the loop `ptr` loses its last reference
// and should be deleted.
}
}
};
You can then use it like this:
Collector collector;
int main() {
// Build your shared pointers:
{
// Allocate them using the collector:
Ref_t c1 = collector.add();
Ref_t c2 = collector.add(c1);
// Then create the cycle:
c1.get()->cycle = c2;
// A normal block with no cycles:
Ref_t c3 = collector.add();
}
// In another scope:
{
// Note: if you run sweep an you still have an existing
// reference to one of the pointers in the collector
// you will lose it since it will be reset().
collector.sweep();
}
}
I tested it with Valgrind and no memory leaks or "still reachable" blocks were listed, so it is probably working as expected.
Some notes on this implementation:
- The memory vector will grow endlessly, you should use some memory allocation technique to make sure it does not occupy all your working memory.
- One may argue that there is no need of using
shared_ptr
(that works like a Reference Counting GC) to implement such a Garbage Collector since the Mark and Sweep algorithm would already take care of the job.
- I did not implement the mark() function because it would complicate the example but it is possible and I will explain it below.
Finally, if you are concerned with (2), this kind of implementation is not unheard of. CPython (the main implementation of Python) does use a mixture of Reference Counting and Mark and Sweep, but mostly for historical reasons.
Implementing the mark()
function:
To implement the mark()
function you will need to make some modifications:
It would be required to add a bool marked;
attribute to Cycle_t
, and use it to check whether the pointer is marked or not.
You will need to write the Collector::mark()
function that would look like this:
void mark(Ref_t root) {
root->marked = true;
// For each other Ref_t stored on root:
for (Ref_t& item : root) {
mark(item);
}
}
And then you should modify the sweep()
function to remove the mark if the pointer is marked or else reset()
the pointer:
void sweep() {
// Run a sweep algorithm:
for (auto& ref : memory) {
// If it still exists:
if (auto ptr = ref.lock()) {
// And is marked:
if (ptr->marked) {
ptr->marked = false;
} else {
ptr->cycle.reset();
}
}
}
}
It was a lengthy explanation, but I hope it helps someone.