Root Pointer was designed to be easy to use and to be versatile in terms of object variants it can refer to. Its only requirement is limited to the usage of a special type needed to instantiate objects referred to.
For example:
root_ptr<int> p = make_root<int>(11);
will instantiate a special object having an integer as one of its member. The
pointer to the object is then passed to the root_ptr<int>
that will manage its existence and later destroy and deallocate it when it
is found to be no longer referenced.
The root_ptr<int>
guarantees
all associated allocations, cyclic or not, will be freed upon its destruction.
Once the root is defined, we can derive a node_ptr<int>
from it:
node_ptr<int> q = make_node<int>(p, 12);
A node_ptr<int>
is an
internal pointer in a set of objects which uses the information of the associated
root_ptr<int>
to define
its length of existence. As its name suggests, this can be used, for example,
as node pointers inside a container for a given root.
Note | |
---|---|
A |
Note | |
---|---|
The first parameter of a |
Note | |
---|---|
|
For example:
struct list_node { list_node(node_proxy const & x) : prior(x), next(x) { } ~list_node() { } node_ptr<list_node> prior; node_ptr<list_node> next; }; struct list { void clear() { root.reset(); } void insert() { if (root.get() == 0) { root = make_root<list_node>(root); // 'root' is used 1 time here } else { root->next = make_node<list_node>(root, root); // 'root' is used 2 times here root->next->prior = root; root = root->next; } } root_ptr<list_node> root; };
Notice that list::root
is actually a root_ptr<list_node>
and therefore a make_root<list_node>
must be used to assign a value to it. Likewise
list_node::next
is in turn a node_ptr<list_node>
and thus a make_node<list_node>
must but used this time with the same
root
parameter twice because:
node_proxy
and propagated to the constructor of node_ptr
list_node
That's it! You now have a cyclic container that will implicitly get destructed
with no need to iterate through all list_node
s.
See root_ptr_example1.cpp for different cases of its usage.
A function can be written to use as an R-value:
root_ptr<int> foo() { return make_root<int>(9); }
and called
std::cout << * foo() << std::endl;
to output
R-value: 9
Slicing is shown in
struct B { int i; B() : i(9) {} }; struct C : B { }; void bar(node_ptr<B> p) { std::cout << p->i << std::endl; }
and called
root_ptr<C> p = make_root<C>(); bar(p);
with output
Slicing: 9
Sharing is shown in
root_ptr<int> p = make_root<int>(9); root_ptr<int> q = p; std::cout << "p: " << *p << std::endl; std::cout << "q: " << *q << std::endl;
with output
Sharing: p: 9 q: 9
Although it is not recommended to do so, it is possible to intermix root_ptr
and node_ptr
with their associated make_root
and make_node
:
root_ptr<int> rp1, rp2, rp3; node_ptr<int> np1(rp1.proxy()); // normal assignment: rp1 = make_root<int>(1); // the following will unify the new 'node_proxy' of 'rp2' with 'rp1' and // assign the pointer to 'rp1': rp1 = make_node<int>(rp2, 2); // the following will create a new 'root_ptr' but immediately slice it into a 'node_ptr' and // still use the 'node_proxy' of 'rp1': np1 = make_root<int>(3); // normal assignment: np1 = make_node<int>(rp3, 4);
In the case where a cyclic set is being destroyed, in order to prevent root_ptr
s member pointers from accessing
an object that has already been destroyed the function cyclic()
is provided to explicitly check the state
of the pointee object:
struct A { root_ptr<A> p; void foo() {} ~A() { if (!p.cyclic()) { p->foo(); } } }; root_ptr<A> p; p = make_root<A>(); p->p = p;
Warning | |
---|---|
|
Creating pointee objects in a faster way is possible by calling make_fastroot
:
root_ptr<int> p = make_fastroot<int>(10);
You can actually use the allocator of your choice using the following function calls:
root_ptr<int> p = allocate_node<int>(make_node_allocator<fast_pool_allocator, int>(), 12);
Hierarchies with multiple inheritance without virtual tables will cause undefined behavior if a pointer to a derived class is assigned to a pointer of one of its base class. For example:
struct M { int i; }; struct N { int i; }; struct O : N, M { int i; }; root_ptr<O> po = make_root<O>(); root_ptr<N> pn = po; // Incorrect! root_ptr<M> pm = po; // Incorrect!
A way to bypass any problem that might be created by the example above is to add virtual destructors to the classes:
struct M { int i; virtual ~M() {} }; struct N { int i; virtual ~N() {} }; struct O : N, M { int i; }; root_ptr<O> po = make_root<O>(); root_ptr<N> pn = po; // Correct. root_ptr<M> pm = po; // Correct.
It is important to point out that even if node_ptr
s
will get destroyed regardless cycles, it is possible to create a cyclic root_ptr
s which will not behave as expected:
#include <iostream> #include <boost/smart_ptr/root_ptr.hpp> using namespace std; using namespace boost; struct A { node_ptr<A> p; A(node_proxy const & x) : p(x) { } ~A() { cout << "~A()" << endl; // will get called } }; struct B { root_ptr<B> p; ~B() { cout << "~B()" << endl; // will not get called } }; int main() { { // 1: root_ptr<A> x; x = make_root<A>(x); x->p = x; } // deterministic destruction { // 2: root_ptr<B> x; x = make_root<B>(); x->p = x; } // no destructor will be called return 0; }
In the first occurrence of root_ptr<A>
x
, the node_proxy
information is properly propagated and therefore the associated node_ptr
s will be correctly detroyed.
In the second occurence of root_ptr<B>
x
, struct
B
is designed to accept a root_ptr
which means it expects node_ptr
s to be its children. If a root_ptr
is assigned to another one, be aware
that cycles will not be detected in this case and thus no destruction will
occur in the aforementioned example.