smart pointers finally
- By Giuseppe Puoti
- lun 03 marzo 2014
the problem
Finally i've convinced myself to move to them. It's one of those thinks that you fear without a real reason, even if you know they are useful and not so difficult to use, you delays their adoption excusing yourself telling of too tight deadlines or too deep modification required.
My trigger was a stupid error about not clear object's ownership assignment. For example, suppose you are implementing a Composite pattern:
class Composite {
list<Component*> components;
void add(Component *c) {
components.push_back(c);
}
virtual ~Composite () {
while(!components.empty()) {
Component* c = components.top();
delete c;
components.pop_front();
}
}
};
void main() {
Composition composit;
Component c1, c2;
}
This minimal example ends with an application crash while exiting from main function because we are trying to delete components two times. One destruction happen because of their allocation on the call stack and the other because of the destructor of the Composite witch, in turn, destroy its components.
what i'd like
The obvious would be use shared_ptr from C++11 standard library but i'm constrained to use quite old compilers that are not compliant with the new standard. Moreover, i'd like to use objects with a semantic similar to the one implemented by modern OO languages such as Java or D with as little syntax pain as possible. I would like to write something such as:
SmartTestClass obj1 = create(SmartTestClass);
SmartTestClass obj2 = obj1;
SmartTestClass obj3;
This requires some helper definition and a little of magic while new object class are defined.
a Smart (or not?) implementation
The example states that a SmartTestClass object is able to:
- Handle a newly created object.
- Be used as a reference without any special declaration.
- Or be declared without an object to handle.
Moreover, what is implicit in the example is the fact that objects are automatically deleted when they are no more useful. That is no more SmartTestClass objects refers to them. In Other words, any SmartTestClass object will share the property of the referred object with any other referring to it while it continue to handle that object.
This can be done using a wrapper class that can wrap any object and tracing for each object wrapped the number of wrappers. This is clearly a template class, i called it Smart:
template <typename T>
class Smart {
static map<T*, unsigned int> counters;
private:
typedef T wrappedType;
T* p;
/*...*/
};
It has just one instance variable storing the pointer to the wrapped objects, the one that actually will response to incoming messages. There is also an instance attribute, a map that store, for each wrapped object, the number of reference. When a Smart object will start handle an different object it will increase the entry in the map corresponding to that object. When a Smart object handle no more an object will decrease the corresponding entry in the map, if this entry goes down to zero, the wrapped object is destroyed.
But when a wrapper object acquire the shared property of a wrapped object? This may happen as:
- the wrapper is created specifying a wrapped object as a constructor parameter
- the wrapper is copy created
- the object is assigned to the wrapper
- a different wrapper of the same wrapped object is assigned to the wrapper
In every of this cases, except for the first, the shared property of any previously wrapped object must be also released. So here is the partial public interface of my Smart class:
template <typename T>
class Smart {
/* ... */
public:
Smart();
Smart(T* p);
Smart(const Smart<T>& other);
virtual ~Smart();
T& operator=(T& obj);
Smart<T>& operator=(Smart<T>& obj);
};
The simple behavior of a wrapper object is to proxy method calls to the wrapped object. That is, modify methods dispatch. The first idea that come in mind is to overload the . (dot) operator. Unfortunately this is one of that few thinks that C++ dosn't allow.
But, what you can do is overload the operator-> behavior that let us send message to objects as as they are handled through a pointer (in fact they are, a Smart object is actually a Smart pointer to the object).
So i've added the overloaded operator-> to Smart class with this simple implementation:
template <typename T>
class Smart{
public:
/* ... */
T* operator->() const {
return p;
}
};
In my opinion this behavior required to the operator to let the object work as a proxy to the wrapper object (pointer), is quite strange. What happen is that the compiler expands calls to operator-> so that a call like this:
class Object {
public:
void method();
}
Smart<Objtect> sm(new Object);
sm->method();
will become at compile time somethink like:
sm->()->method();
On this code snippets i'm cheating, omitting something is actually required to let objects of a particular class to be available to be wrapped into a Smart object. You have noticed that the Smart class has a static map attribute to store the number of references for each wrapped pointer. But the Smart class is a template one so, every each time you try to wrap an object belonging to a different class, a new map is declared and but it is up to you to provide that map instance (usually you would instantiate it in your object implementation file).
So you should provide an instantiation (and only one!) like this every each time you decide to wrap an object.
std::map<Object*, unsigned int> Smart<Object>::counters;
This is not a real nice think to do but it is necessary in this implementation and usefull to leave as decoupled as possible wrapper object from wrapper one or, in other words, to let any object be wrappable.
I will overtake it so stay tuned!
a little of magic
Till now, my Smart implementation require still some ugly code to be written. I now want to simplify this. First of all notice that there is a typedef in the Smart class so that every wrapper class instantiated is linked to the class of objects it wraps.
This makes possible to introduce an helper function that let me create a wrapped object based on the type of objects it wraps.
template<typename WrapperT> WrapperT SmartCreate(){
return WrapperT(new WrapperT::wrappedType);
}
This only requires that the wrapped object has a default constructor.
With this little magic introduced i can instantiate my Smart objects with something like:
Smart<Object> sm = SmartCreate<Smart<Object>>();
Of course it is ugly too but a typedef may help. When i try to wrap an object of a certain type for the first time, what happen is that i'm instantiating a new Smart class from the template. As you know, it is actually a different type for the compiler and i can let this think be more clear to myself too using a typef:
typedef Smart<Object> SmartObj;
SmartObj sm = SmartCreate<SmartObj>();
To avoid angular parentesys, just use a macro:
#define create(SMART_CLASS) SmartCreate<SMART_CLASS>();
SmartObj sm = create(SmartObj);
So creating a Smart object has become as simple as instantiating a similar object in other languages such as Java or C# or even D.
class Main {
public static void main(String args[]){
Object obj = new Object();
}
}
a little more of magic
Unfortunately before a Smart object can be used, still some ugly code is required. You need to:
- instantiate the Smart<Object>::map
- declare the a new type for the wrapper class
Two macro will help to state what we want to do with the wrapped object instead of deal with implementations details:
#define DECLARE_SMART(__CLASS_NAME__, __SMART_NAME__) typedef Smart<__CLASS_NAME__> __SMART_NAME__;
#define IMPLEMENT_SMART(__CLASS_NAME__) map<__CLASS_NAME__*, unsigned int> Smart<__CLASS_NAME__>::counters;
with those two macro, when i want a to use Objects of a certain class in a smart way, i state somethink like:
/* NotSmart.h */
Class NotSmart {
public:
/* ... */
void callMe(){
/* ... */
}
}
/* MyObject.h */
DECLARE_SMART(NotSmart, MyObject )
void exec(MyObject& obj);
/* MyObject.cpp */
IMPLEMENT_SMART(MyObject)
void exec(MyObject& obj){
obj->callMe();
}
int main(int argc, char* argv[]){
MyObject obj = create(MyObject);
exec(obj);
return 0;
}
This is my very first i smart pointers implementation, it aims to be generic but i know it is quite limited. The major limitation i see at this point is that it do not support polymorphism, for example, you cannot write thinks such as:
/* MultipleObjects.h
class A {
public:
/** some interface methods **/
};
class B : public A {
public:
/* ... */
};
DECLARE_SMART(A, SmartA)
DECLARE_SMART(B, SmartB)
/* MultipleObjects.cpp */
IMPLEMENT_SMART(SmartA)
IMPLEMENT_SMART(SmartB)
/* implement methods for A and B */
#include "MultipleObjects.h"
int main (int argc, char* argv[]){
SmartB b = create(SmartB);
SmartA a = b;
return 0;
}
What happen here is that the SmartB object created by create(SmartB) is not assignable to SmartA because the inheritance relation between A and B do not imply any inheritance relation on Smart template class instances. This is a limitation overcome but i will discuss it in a different post to not overload too much this already too long. Stay tuned.