Strategies for Writing and Debugging Code in Slicer 3

From Slicer Wiki
Revision as of 22:34, 17 June 2007 by Klawson (talk | contribs) (→‎Managing Issues with Memory)
(diff) ← Older revision | Latest revision (diff) | Newer revision → (diff)
Jump to: navigation, search
Home < Strategies for Writing and Debugging Code in Slicer 3

Managing Issues with Memory

The following section outlines answers to common scenarios encountered during Slicer 3 development.

What is a memory leak?

Memory leaks occur when a section of code allocates a block of memory that is never reclaimed.

void MyFunction()
{
   short *buffer = new short[1000]; // this memory is leaked!
}

This example is rather contrived. There more insipid cases where it is very unclear that memory is not being reclaimed. Most memory leaks occur where several objects are referencing the same block of memory with each object assuming some other object is responsible for deallocating the memory.

How does memory get deleted multiple times?

A frequent problem with passing pointers from routine to routine is that it becomes very easy to loose track of which routine is responsible for deleting the block of memory. APIs rely on their documentation to educate the developers as to whether the developer is responsible for deleting the memory.

short *buffer = mykit::CreateBuffer(1000);
...
myKit::CleanUp();
...
delete [] buffer; // did myKit::CleanUp() already delete this memory?

Memory can be deleted multiple times when it is particularly unclear as to whether the memory was allocated on the heap or the stack.

Reference Counting

VTK and ITK both use reference counting to keep track of outstanding references to an object and automatically delete an object when it is no longer needed. VTK does with via calls to New()/Delete() and Register()/UnRegister() to increase and decrease the number of references to an object. If a function call returns a pointer to an object, the caller can choose to keep a long term handle to that object by increasing the reference count. The caller is responsible for decreasing the reference count when they no longer need the object.

 MyClass::Function1()
 {
    this->SomeObject = MyOtherClass::GetObject();
    this->SomeObject->Register( this );
 }

 MyClass::~MyClass()
 {
    if (this->SomeObject)
      {
      this->SomeObject->UnRegister();
      this->SomeObject = 0;
      }
 }

Stack-based coding

One way to avoid memory leaks is to avoid allocating objects on the heap. Instead objects are allocated directly on the stack and deallocated from the stack using standard scoping rules.

 MyObject::Function()
 {
   MyOtherObject* object1 = new MyOtherObject();    // object1 allocated from the heap.  
                                                    // The developer must reclaim this memory later or it is a leak.
 
   MyOtherObject object2();                         // object2 allocated on the stack. 
                                                    // Memory reclaimed automatically when the object goes out of 
                                                    // scope (in this case at the end of the function).
 }

The Standard Template Library (STL) promotes which type of coding. To use STL safely, all objects stored in STL containers should be default constructible and have proper copy constructors, etc. This is needed so the containers can shuffle objects in memory, reallocate space, etc. When STL containers of objects go out of scope, the destructor of the vector calls the destructor of each object it contains. If these vectors are vectors to pointers to objects, the destructors of each object are not called.

 MyObject::Function()
 {
  std::vector<MyObject*> vec;
  
  vec.push_back( MyOtherClass::GetObject(0) );
  vec.push_back( MyOtherClass::GetObject(1) );
 }  // destructors are not called on items pointed to by elements of the vector

SmartPointers

ITK has used SmartPointers since its beginning to simply the reference counting semantics. With SmartPointers, the reference count of an object is automatically increased when assigned to a SmartPointer and automatically decreased when unassigned from a SmartPointer. Objects are unassigned from a SmartPointer whenever that SmartPointer is assigned to another object or to 0. The latter occurs automatically whenever a SmartPointer goes out of scope.

 MyObject::Function()
 {
    itk::Image<short, 2>::Pointer smartPointer = itk::Image<short,2>::New();  // creates a new image

    smartPointer = MyOtherClass::GetImage();  // reference count of the image previously pointed to by 
                                              // smartPointer decremented. Reference count on image returned 
                                              // by MyOtherClass::GetImage() incremented.


 }  // smartPointer goes out of scope, reference count of the image returned by MyOtherClass::GetImage() decremented.

Because SmartPointers abide by scoping semantics, they can be used with STL containers.

   std::vector<itk::Image<short, 2>::Pointer> vec;

   vec.push_back( itk::Image<short, 2>::New() );
   vec.push_back( itk::Image<short, 2>::New() );

   vec = std::vector<itk::Image<short, 2>::Pointer>();  // vec is now a copy of an empty vector
                                                        // Reference counts to the images pointed to by the elements
                                                        // of the vector properly managed.

We like SmartPointers in ITK so much, they were added into VTK. The behavior is the same as in ITK. However, the New() in VTK has always returned a raw pointer whereas the New() method in ITK returns a SmartPointer. So a little more setup is needed to use a SmartPointer in VTK.

  itk::Image<short, 2>::Pointer itkPointer = itk::Image<short, 2>::New(); // after this line, reference count is 1

  vtkImageData *image = vtkImageData::New();                              // after this line, reference count is 1
  vtkSmartPointer<vtkImageData> vtkPointer = image;                       // after this line, reference count is 2
  image->Delete();                                                        // after this line, reference count is 1

VTK SmartPointers can be used in STL containers just like ITK SmartPointers.

  std::map<std::string, vtkSmartPointer<vtkImageData> > myMap;

  myMap["foreground"] = a->GetOutput();
  myMap["background"] = b->GetOutput();


What does this mean for Slicer3?

Slicer creates a large number of objects which are subclasses of VTK object. Every image, model, transform, node, module, and GUI component is a VTK object. The referencing counting on these objects is all manually coded. There are many opportunities to use SmartPointers and simplify the code and avoid programming errors. We suggest using SmartPointers where possible.

In ITK, there are a set of guidelines for using SmartPointers.

  1. If you need a persistent handle to an object, store it in a SmartPointer.
  2. Functions take raw pointers as parameters and return raw pointers as return values.
    1. The exception to this rule is if the routine creates the object but does not hold onto a persistent reference itself. In this case, the routine must return a SmartPointer.