A gcroot
represents a smart pointer that retains shared protection
of a heap allocated IObject
which is registered in a CSpace
.
Unlike a std::shared_ptr<T>
where T
is the element type,
a gcroot<P>
is parameterised in the pointer type P.
For example the pointer type can be a normal memory pointer, or an interface pointer:
gcroot<T*>
can point at an object whose concrete type is any subclass of T
gcroot<ptr<I>>
can point at an object which implements interface I
The shared protection of the object means it is protected from being destroyed by the CSpace
tracing garbage collector.
We call it a "gc root" because it represents a root object for the tracing garbage
collector. In other words, the gcroot
smart pointer not only protects the referenced
object, it also protects all objects which are reachable from that object.
A gcroot
may also own no objects, in which case it is called empty.
This is the case for a default constructed gcroot
. A gcroot can be made empty by
calling reset()
with no arguments, by calling reset(nullptr)
or by
assigning with nullptr
.
gcroot
variables may for example be:
In the following example there is a gcroot
variable named p
declared on the frame.
Variable p
protects the object it references during its lifetime.
In particular, the call to the method foo()
is made without fear of the object
being destroyed, even though the CSpace
is no longer locked when
foo()
is called.
gcroot<X*> p;
{
CSpaceLock lock;
p.reset($new X(2.3, false, "hello world"));
}
p->foo();
The shared protection of an object using gcroot
is analogous to the shared ownership using
std::shared_ptr<T>
except a gcroot
is used to stop an object from being
garbage collected whereas a std::shared_ptr
is used to ensure an object is deleted.
Indeed the following code creates a memory leak, because the object instance has been allocated with
new
instead of $new
and therefore has not been registered in a
CSpace
. The gcroot
protects the object from being garbage collected
but it isn't going to be garbage collected anyway.
gcroot<X*> p(new X);
If pointer type From
is convertible to pointer type To
(i.e. std::is_convertible<From,To>::value
is true)
then gcroot<To>
supports copy construction and copy assignment from
gcroot<From>
.
This creates multiple pointers to the same object, each of them protecting the shared object from
being garbage collected.
gcroot
instances can be freely constructed, destructed, copy constructed and copy assigned without regard for
CSpace
locks. These operations don't acquire CSpace
locks and they don't assume the calling
thread either has or doesn't have a CSpace
lock.
All member functions (including copy constructor and copy assignment) can be called by multiple threads on
different instances of gcroot
without additional synchronization even if these instances are copies and share ownership
of the same object.
If multiple threads of execution access the same gcroot
without synchronization and any of those accesses
uses a non-const member function of gcroot
then a data race will occur.
An object that is or becomes unreferenced by any gcroot
inside a CSpaceLock
is nevertheless
protected from garbage collection during the CSpaceLock
.
For example, in the following the object is temporarily unprotected yet it cannot be garbage collected:
void f(gcroot<X*> r)
{
{
CSpaceLock lock;
X* p = r.get();
r.reset(); // Unprotect the object
r.reset(p); // Protect it again
}
r->foo(); // Safe
}
Indeed this is the state of affairs for the object returned by invoking $new
.
At that point in time the object is unreferenced, but is nevertheless protected from garbage collection during
the CSpace
lock.
If an object becomes referenced by a gcroot
before the CSpace
lock is released then that
guarantees the object will not be garbage collected.
Note that the following is not assignment, it is copy initialization:
gcroot<X*> p = $new X; // compiler error
It doesn't work because the constructor of gcroot
taking a pointer type is marked as explicit:
template<typename T>
class gcroot
{
public:
template <class R> explicit gcroot(R p) noexcept;
...
};
Assignment from a raw pointer doesn't work either, because gcroot
doesn't have an overloaded
assignment operator taking a raw pointer as parameter.
gcroot<X*> p;
p = $new X; // compiler error
Instead, the overload of reset
taking a raw pointer can be called on a gcroot
to
make it take ownership of an existing object.
The free function make_gcroot
is provided for convenience in allocating a object,
registering it in the CSpace
and returning a pointer to it using a gcroot
.
template <class T, class... Args>
gcroot<T*> make_gcroot(Args&&... args);
The following illustrates how it can be used:
gcroot<X*> p;
{
CSpaceLock lock;
p = make_gcroot<X>(2.3, false, "hello world");
}
p->foo();
make_gcroot<T>(...)
is like $new T(...)
in that:
T
must be a concrete type which implements the IObject
interfaceCSpace
which is set in thread local storageIObject
of type T
from the heapCSpace
CSpace
garbage collector by
calling the IObject
method Destroy()
However make_gcroot<T>(...)
returns a gcroot<T*>
whereas $new T(...)
returns a raw T*
.
make_gcroot<T>(...)
perfect forwards the arguments onto the T
constructor in a similar way
to std::make_shared
.