smarter pointers
- By Giuseppe Puoti
- mer 26 marzo 2014
In a previous article, I've start discussing about a Smart pointers implementation that aims to let me use C++ as a higher level language with some minimal automatic garbage collection functionality. I know that someone will disagree with this approach and also I quite agree with ones that recommend to use Java (or C# or whatever) if it is what you want or what you need, but, sometime you simple can't get what you want or what will possibly be the best choice.
So i still find some good reasons that make me say it's useful to prepare some facilities that helps me use C++ in a simple, less error prone, and more productive way.
- you can't decide by yourself
- In my experience this happen because these languages are wrongly perceived as slow by management and, even if the inefficiency of our code is mostly determined by our algorithms and container we use, speed requirements is used as rationale to write code in C or C++.
- still can't decide by yourself
- Moreover sometime a new language and probably new framework are also difficult to accept for colleagues that possibly are far more ready to be productive with the good old C++.
- do not reinvent the wheel
- Finally (but this could be the most technically important problem), you probably have large codebase not ready to be used from languages you'd like to adopt.
So, even if, in general terms, you want (and need) Java (or whatever), you could not get it, but you must to use C++. In this cases using it in a way that is not the most "right" or efficient but the most productive, could be the best compromise.
adding inheritance capabilities
A strong limitation of the implementation discussed last time, is the unavailability of upcast and polymorphic behavior of my implementation of smart pointers. So, let start make the pointers smarter with a test. I'll write them using gtest.
TEST(smart_derived, assignToSuperclassWrapper){
Object o2;
{
Derived o1 = SmartCreate<Derived>();
o1->set("foo");
o2 = o1;
}
ASSERT_EQ(o2->get(), "foo");
With the old implementation of smart pointers this would not work, instead the application will crash with a segmentation fault because of an wrong reference counting. In fact only references with the same type of the actually created object were taken into account. Do you remember we had a static map...
template <typename T>
class Smart {
static map<T*, unsigned int> counters;
/* ... */
So when Derived object o1 is assigned to o2, it count one reference to the actual object as a Derived and 1 reference as Object. When, in the example, o1 exits its scope, its reference counting drop to 0 and it is destoyed even if it is still referenced as a simple Object.
Remember also that we want the object to be shared, so, all the references must share the same actual object, make a clone as the object get assigned is not an option even if we didn't care about performances.
What we must do is count any reference to an object regardless of its type. So we will have only one reference counter table shared by all reference counter type. Because i don't like to have variables at global scope (ok, static variables are global too but i like them more ^_^ ) I've introduced a superclass that all smart ptr classes derive from that will do the job of reference counting and deallocating object when required.
I've chosen Smarty as a cool name for my smarter pointers. So the superclass of all the Smarty template class instances, will be Smarties.
class Smarties{
protected:
static std::map<void*, unsigned int> counters;
template<typename T> void decreaseRefs(T* p){
if(p != NULL){
std::cout<< "decreasing counter " << --counters[(void*)p] << std::endl;
if(counters[(void*)p] == 0){
delete p;
}
}
}
void increaseRefs(void* p){
typename std::map<void*, unsigned int>::iterator it;
if(p != NULL){
it = counters.find(p);
if(it != counters.end()){
++counters[p];
std::cout<< "increasing counter " << counters[p] << std::endl;
}
else{
std::cout << "first counting "<< std::endl;
counters[p] = 1;
}
}
}
};
All of its member are protected because i need to use them from Smarty instances.
The two methods that Smarties can accept are really self explanatory, them receives a pointer as unique parameter. The type of these pointers is not important but The decraseRefs method, when necessary, also delete the object and delete operator do not like void* that much, so i've written it as a template method and used cast to void* to search into the map. Please notice that this cast is really not problematic.
The Smarty implementation
It is quite similar to the one of my old Smart objects. But I've added some methods to let implicit upcast work as you could expect. Here follows an extract.
template <typename T>
class Smarty : public Smarties {
public:
typedef T wrappedType;
private:
T* p;
void increaseRefs(){
Smarties::increaseRefs(p);
}
void decreaseRefs(){
Smarties::decreaseRefs(p);
}
void assign(T* np){
decreaseRefs();
p = np;
increaseRefs();
}
public:
/* ctors and methods similar to Smart */
template<typename K> Smarty<T>& operator=(const Smarty<K>& obj){
T* tmp = obj.operator->();
assign(tmp);
return *this;
}
Smarty<T>& operator=(const Smarty<T>& obj){
T* tmp = obj.operator->();
assign(tmp);
return *this;
}
template<typename K> operator Smarty<K>(){
return *this;
}
};
I have some private helper methods and the old assignment operator. I will discuss later why still have that assignment.
But Smarty has more capabilities. Because of its template assignment method, any Smarty wrapper that is constructed around something that is assignable to an object of the type wrapped by the actual Smarty, is assignable to the Smarty wrapper in turn.
The only assignment operator makes te test written on top of this post pass but not the following one.
TEST(smart_derived, assignToSuperclassWrapper){
Object o2 = SmartCreate<Derived>();
{
Derived o1 = SmartCreate<Derived>();
o1->set("foo");
o2 = o1;
}
ASSERT_EQ(o2->get(), "foo");
This is because of a compile time error at the first test line: conversion from ‘Smarty<DerivedImpl>’ to non-scalar type ‘Object {aka Smarty<ObjectImpl>}’ requested. I've simply given the compiler what it were asking me with the conversion method using the fact that the upcast assignment operator is in place yet.
about the old assignment
It is still not clear to me why but, after having substituted the old assignment operator with the template one i started to no more pass the this test:
Object o2;
{
Object o1 = SmartCreate<Object>();
o1->set("foo");
o2 = o1;
}
ASSERT_EQ(o2->get(), "foo");
}
After some debug i've solved by mainteining the old assignment operator, but, to be honest, it is still not clear to me why this works.