This is the sixth and final post in a series on implementing parent – child and multi-level parent – child relationships in C++. The previous posts are:
- Implementing Parent – Child and Similar Relationships
- Parent – Child Implementation: Raw Pointers
- Parent – Child Implementation: shared_ptr
- Parent – Child Implementation: unique_ptr
- Multi-Level Parent Child Implementation: shared_ptr
This post compares the various implementations for parent – child relationships: using raw pointers, using shared_ptrs, and using unique_ptrs. It also looks at the shared_ptr implementation for multi-level parent – child relationships. Finally, potential alternatives to these mechanisms are discussed.
In the raw pointer implementation, the intent is that the Parent object owns its children (Child objects). However, this is not shown in any way except possibly in documentation. Looking at the code, a raw pointer is created when a Child object is created. Ownership of the Child object is transferred to the Parent object either in the Child constructor, or by calling Parent::addChild. But of course, the original pointer to the Child object still exists until that pointer goes out of scope. Both Parent::findChild and Parent::removeChild return a raw pointer to a Child object, but only Parent::removeChild transfers ownership back to the caller. As a result, there can be multiple pointers to the same Child object. It is left to the programmer to ensure that Child objects are destroyed appropriately, either automatically by the owning Parent object, or directly in her code for Child objects not owned by a Parent object. Any logic errors in the program can result in the code attempting to destroy a Child object multiple times (double deletion), or not at all (memory leak).
This situation is why smart pointers were invented. Let’s look first at the unique_ptr implementation. In this case, there is exactly one owner of a Child object and it is obvious who that owner is. The program must still ensure that the unique_ptr is valid before attempting to use it. The Child object is destroyed when the unique_ptr goes out of scope.
There is one problem area, however. Parent::findChild cannot return a unique_ptr to the Child object because the intent is for the Child object to still be owned by the Parent object; a raw pointer is thus returned from that method. The program must not delete the Child object pointed to by the raw pointer as that would leave a unique_ptr that points to a destroyed object.
Finally, in the shared_ptr implementation, Child objects have shared ownership. All references to a Child object are via shared_ptrs, even Parent::findChild. The underlying Child object is destroyed only when the last smart pointer to the object is destroyed.
Transfer of Ownership
In the raw pointer implementation, there are two ways of transferring ownership of the Child object to a Parent object: in the Child constructor, or via Parent::addChild.
In both the shared_ptr and unique_ptr implementations, ownership transfer to the parent is via the static method, Parent::addChild. A static method is used because smart pointers are required for both the Parent and Child objects; there is no way to get to the smart pointer to an object ( the Parent object) directly from that object. As I stated a couple of times in the implementation posts, I find static methods esthetically unsatisfying except possibly for builder and factory methods. That is a personal opinion; you, of course, may take the opposite view.
In the shared_ptr implementation, it may be possible to combine creation of the Child object with transfer of ownership to the Parent; a shared_ptr to the created Child object would be returned to the calling code. If this were tried in the unique_ptr implementation, a raw pointer to the Child object would have to be returned to the calling code because the Parent object must hold the unique_ptr to the Child object.
In all implementations, a Parent::addChild method (either static or not) is still required.
Separation of Responsibilities
The Child constructor in the raw pointer implementation performs two functions: construction of the Child object and possibly transfer of ownership of the object to a Parent object. Alternatively, a Child object can be constructed, and then via a call to Parent::addChild, ownership of the Child object can be transferred to the Parent object.
Performing both tasks in the constructor may be viewed as a violation of the separation of responsibilities, aka separation of concerns. In the smart pointer implementations, ownership transfer must be performed separately. Note however, that modified implementations of the smart pointer solutions could combine Child object construction and ownership transfer in a single method again violating separation of responsibilities.
Performing both tasks in a single method (constructor or builder method) hides some of the details at the wrong level of abstraction. Any programmer reading the code would need to know that the constructor (in the raw pointer implementation) does not just create a Child object but also transfers ownership of the object. This is not obvious because a constructor normally just creates an object and initializes its internal values.
Multi-level Parent – Child Relationships
The multi-level relationship is a generalization of the parent – child relationship. The only difference is that each Child object can in turn contain other Child objects, thereby functioning as both a Child object and as a Parent object.
The code provided in the implementation post assumes that all parent and child classes derive from Base; that is, every object created can have children. This normally does not occur. For instance, in a windowing system, there are container widgets (e.g. top-level windows, panels, group boxes) that can contain other widgets, and controls (e.g. static text, buttons, edit controls, etc.) which cannot contain other widgets. In an n-ary tree implementation, there are normally two node types: leaf nodes and non-leaf nodes. Only the non-leaf nodes can have children (other nodes) attached to them.
Alternative Implementations of Parent – Child Relationships
This set of six posts used raw pointers, shared_ptrs and unique_ptrs to implement parent – child relationships. Only a shared_ptr implementation was provided for multi-level parent – child relationships. However, raw pointer and unique_ptr implementations of multi-level relationships are also possible. They, of course, suffer from the same problems that the raw pointer and unique_ptr implementations of parent – child relationships do, so see those posts for more information.
Alternative implementations are possible. We will discuss two: references to objects, and embedded reference counting.
References to Objects
In the raw pointer implementation of the parent – child relationship, each parent and child object was created on the heap. Alternatively, these objects could be created on the stack either within main, embedded in objects created within main, or as global objects. Instead of passing the objects via raw pointers, the objects would be passed by reference. Internally in the Parent and Child objects, the references would be stored as pointers. Returns from methods such as Parent::findChild and Parent::removeChild would be via raw pointers. Parent objects don’t own their children, simply have pointers to them, and the children are destroyed when they go out of scope.
The programmer must remember to never delete any Child object returned as a pointer.
Embedded Reference Counting
Before smart pointers were added to C++, Parent and Child classes often were implemented with internal reference counting. See for example, wxWidgets and Qt. The details are hidden from the programmer, but as long as the Parent and Child objects are created on the heap, the reference counting mechanism ensures that the objects are not deleted as long as they are being referenced.
Library designers today should consider implementing reference counting using smart pointers rather than rolling their own reference counting mechanisms.