Intro | Compiling | Memory management | Minimal programme | EM Sources

Memory Management

Reference counting and memory management

Note
Previous versions of Lemma used an internal reference counting mechanism, what is described below represents a significant change in the structure, philosophy, and internals of Lemma. To a large extent this has been possible due to changes in the C++ language specifications since C++-0x. These changes should improve the readability of Lemma code, decrease errors, improve performance, and reduce code length. The C++-17 standard promises to close a remaining oddities in the Lemma API, discussed below.

Lemma relies heavily on smart pointer which utilize internal reference counting for several reasons:

  • It is not uncommon for multiple objects to have the same instantiations of an object as members.
  • Making copies of each object is not a viable option because the objects may be large, and they must all update simultaneously.
  • Without reference counting it is very easy to leave dangling pointers.
  • It is much easier to write code, as local copies of objects can be deleted as soon as they are no longer used within that context. The memory will not be freed and the object persists as long as other objects still reference it.

The C++-0x standard introduced std::shared_ptr as a widely available, standard-compliant, high performance option to use in instances like this. The other smart pointer types specifically weak_ptr and unique_ptr are less handy in our application. As such, all Lemma classes expose a factory method returning a shared_ptr as the only publicly available means by which to construct objects. Internally, in cases where higher performance can be realized, manual memory management is employed.

The base class for all Lemma objects is LemmaObject. The interface requires that all derived, non-abstract classes have NewSP and DeSerialize methods. These methods are the only mechanisms by which you should be creating Lemma objects.

std::shared_ptr< DerivedClass > Object = DerivedClass::NewSP();
// Or alternatively
auto Object2 = DerivedClass::NewSP();
Warning
It is important to note that the default constructors and destructors are inaccessible so the following code is NOT valid.
DerivedClass* Object = new DerivedClass;
delete Object;
Interestingly, the default constructors and destructors are public, however they are locked using a key struct (link). The reason that this is necessary, is this arrangement allows for the use of std::make_shared to construct objects. The performance increase of this was chosen over the higher overhead associated with making the default methods protected.

Many Lemma classes have other Lemma classes as data members. Classes may share these members, and depend on them being updated simultaneously. So that a situation like this is not unusual.

DerivedClass* Component = DerivedClass::New();
AnotherDerivedClass* Object2 = AnotherDerivedClass::New();
YetAnotherDerivedClass* Object3 = YetAnotherDerivedClass::New();
Object2->AttachMyComponent(Component);
Object3->AttachMyComponent(Component);
Component->Update(); // All three classes use the updated Component

At this point both Object2 and Object3 have an up to date Component and any changes to Component can be seen by the Objects. Often these types of connections are happening behind the scenes and end users should not be troubled by any of this.

Now in this example it is clear that if Component is deleted, than the objects will contain dangling pointers. To avert this, calling the ReferenceCountedObject::Delete() does not necessarily destroy the object. So that it would be safe – continuing from the above example

Component->Delete(); // The 'Component' handle will no longer be used.
Object2->CallMethodRelyingOnComponent(); // But we can still use the object

Whats going on?

Whenever we declared a new handle to the object– either by calling New, or implicitly in the Connect methods– the true 'Component' object updated a list of pointers that had handles to it.

DerivedClass* Component = DerivedClass::New(); // One Reference, 'Component'
Object2->AttachMyComponent(Component); // Two References, internal in Object2
Object3->AttachMyComponent(Component); // Three References, internal in Object3
Component->Delete(); // Two References, 'Component' is gone.
// DON'T USE Component->Method() ANYMORE!!!
Object2->Delete(); // One Reference, Object2 releases handle upon Delete
Object3->SomeFuntionReleasingComponent(); // Zero References, Component object is now deleted and
// all memory is freed!

So what do I need to know?

Not much. This is here to make life easy when doing high level programming. Just follow these simple rules

  • Allocate and free all variables with New and Delete, you have to do this as the default versions are protected.
  • Once calling Delete on an object it is no longer safe to use that handle. For example don't call Component->Delete(), and then call Component->Method(). Even if you 'know' that there are existing references and that it shouldn't have been deleted yet. Instead move your call to Delete down after your last direct use of each class.
  • Remember to call Delete, thanks to reference counting it is safe to do this as soon as you are done using an object even if other classes are using it. If you don't call Delete on objects you create, your program will leak memory!
  • Revel in the fact that memory is being managed for you, and that dangling pointers are not a concern.