Objective
Memory management of objects, that are used from multiple threads, often lead to access violations, because one thread
MAY delete the object while it is still used by another thread. This led to the thought of defining the lifetime of an object by the existence of atleast one reference to the object.
Design Requirements
- Usage syntax: Since the pointer class is to be retro-fitted into existing code, the changes required in the code should be minimal. So the usage of the smart pointer should resemble the syntax for using a raw pointer as closely as possible.
- Polymorphism support: Most systems use abstract classes to define interfaces that are inherited by multiple classes. This is done to seperate the interface from the implementation. In such systems, there are bound to be containers of interface pointers.
In a polymorphic object, the addresses of different interface pointers are different. Consider a class MyClass
inheriting from IinterfaceA
and IinterfaceB
.
- Problem 1: Smart pointers pointing to the same object can be of different types.
- Problem 2: Some of the types may be abstract and delete cannot be called on those objects without causing slicing.
- Problem 3: Pointers to the same object will have different values.
MyClass* pClass = new MyClass;
IInterfaceA* pIntA = pClass;
IInterfaceB* pIntB = pClass;
In this example,
pClass
,
pIntA
and
pIntB
point to the same object, but their values are different. Why? because each interface is at a different offset in memory.
The solution
Consider problem 3 first. How can the start address of the object be obtained? C++ standard says that dynamic_cast<void*>
always points to the start of the object. If this conversion is applied to the address, before updating the address based reference count or do comparison operations, problem 3 is solved.
But, is it? No, because dynamic_cast
works only on polymorphic types. For non-polymorphic types, the code will not compile. So, dynamic_cast
should be performed only if the object is polymorphic. Run Time Type Information (RTTI)
can be used to do this. There is a keyword typeid
, that evaluates an expression at runtime, only if it is polymorphic. Using a comma expression as follows:
template <typename U>
bool is_polymorphic(U* p)
{
bool result = false;
typeid(result=true, *p);
return result;
}
How does this work? Look at the expression
(result=true, *p)
. This expression always returns *p, the rightmost value. The keyword typeid evaluates an expression only if the type is polymorphic. So result becomes true only when p points to an object of a polymorphic class. The start address of the object can also be obtained similarly.
template <typename U>
void* get_pm_ptr(U* p)
{
void* x = static_cast<void*>(p);
typeid(x = dynamic_cast<void*>(p), *p);
return x;
}
As you can infer, the dynamic_cast is done only if the object is of a polymorphic class.
But, there is one problem. The operator
typeid
throws an exception if p is
NULL
. Hence,
template <typename U>
void* get_pm_ptr(U* p)
{
void* x = static_cast<void*>(p);
if (x != NULL)
typeid(x = dynamic_cast<void*>(p), *p);
return x;
}
Finally, to solve problem 1 and 3, reference count should be maintainted not by type, but by address. This can be done only on a global basis. We create a static object, that maintains a map of addresses to their reference count. The pointer class uses this static object to update the reference count and to find whether the object should be deleted because the ref count is zero.
Problem 2: The Deletion Problem: If the smart pointer is of an incomplete type or if it points to one of the inherited classes, then deletion cannot be done, if the classes do not implement a virtual destructor. So an
IDelete
interface class is provided. It has a single pure virtual function. We manadate that all polymorphic classes, that need to be used with this smart pointer, implement this interface - if it does not have a virtual destructor. The pure virtual function is named
self_delete()
. The implementation of
self_delete
in your polymorphic class should be as follows:
public:
virtual void self_delete()
{
delete this;
}
The smart pointer will now call
self_delete
on the pointer after casting it to type IDelete, if the object implements
IDelete
; otherwise a raw delete is performed.
Warning: If you do not implement IDelete
on a polymorphic class with no virtual destructors, the behaviour is undefined.
Usage Syntax
By overloading the =, ->, ==, !, !=; implementing a consructors that take raw pointers and implementing smart pointer copy constructor, the raw pointer usage syntax is maintained. The only compromise is the C++ casting operators -
static_cast
,
dynamic_cast
etc.
_mm::_ptr<myclass> x = new myclass;
_mm::_ptr<iinterface1> y = dynamic_cast(x);
_mm::_ptr<iinterface1> y = dynamic_cast(x.ptr());
Dependencies
The code should be compiled with the 'Enable RTTI' option in the "Project | Settings | C/C++ tab | C++ language".
It uses STL map class.
Using in your code
Include
smart_ptr.h in your project. The class is called
_mm::_ptr
and is used as
_mm::_ptr<myclass> x = new myclass;
_mm::_ptr<iinterface1> y = dynamic_cast<iinterface1* >(x.ptr());
_mm::_ptr<iinterface2> z = dynamic_cast<iinterface1* >(x.ptr());
z = dynamic_cast<iinterface1* >(x.ptr();
z = NULL;
y = NULL;
x = NULL;
Testing
You can use the test project to do some performance tests. I got approximately 47 nano seconds for an assign operation in my test on a PII/266. Please give me suggestions to improve this further. Particluarly, any attempts at code optimization is welcome<CODE>.
Bug fixes/Updates
- Added an
IDelete
interface to prevent slicing when dealing objects with multiple inherited classes.
- Changed the deletion to delete the object directly, if an implementation of
IDelete
does not exist. This was required because RTTI interpreted polymorpism by existence on one virtual function and most classes have one virtual destructor.
- Added a function
ptr()
to return the raw pointer, so that dynamic_cast
can be performed without assignment to a raw pointer.
- Added critical sections to make the global map access thread safe.
Credits
As pointed out by
William Kempf, the
IDelete
interface is not required if your classes have virtual destructor. If not, the problem is not slicing, the result is undefined. I have retained the
IDelete
in case someone chooses to use this class with a polymorphic class with no virtual destructor.
Greg Vogel pointed out the missing copy assignment operator.
He also pointed out the pointer cannot be passed accross module boundaries, because each module works using a different copy of the static map. Greg has suggested use of a memory mapped file to resolve this issue, which he has already successfully implemented.
Giving more thought to this problem, it seems that the ideal way to pass pointers to other modules or libraries is to pass them as raw pointers. The main application SHOULD maintain a reference counted pointer, until the module needs this raw pointer. It is recommended that modules do not store the raw pointers from smart pointers internally and get them from the main app with every function call. This was the initial reason why rawpointer support was introduced. Any changes to this premise have to be implemented by the user of this class.
Chen Sheng tested the code extensively and provided valuable contributions to fix bugs in the code.
References