Click here to Skip to main content
15,867,975 members
Articles / Desktop Programming / MFC

The Safest Smart Pointer of the East

Rate me:
Please Sign up or sign in to vote.
4.21/5 (14 votes)
28 Oct 2003CPOL12 min read 90.4K   698   39   22
Reference counting smart pointers with automtic type conversions.

Introduction

Oh, well, the title has clearly been inspired by another article "The faster smart pointer of the west", not really because I want to create a counterpart, but simply because I thought that a certain situation not addressed in that implementation should however find an answer.

Smart pointer or smart objects ?

I started to think of smart pointer in terms of "reference counting pointers", but I found some difficulties in the following situation:

  • Circularity. You cannot use ref-counting also for back-reference: a not-releasable loop will be created. In most of the cases a "dumb" pointer is used to close the loop, but … what happens in the case the back-pointed object dies, while the forward-pointed is … two or more times linked? He’s still alive with an invalid back pointer!
  • Aggregation. It happens when various objects (components) act together as a single entity. You can use inheritance, you can use embedding, but – if you have to link to a component – you probably want the entire aggregate to be ref-counted, (not only the component) or you risk to delete "pieces of object".

So, I thought to implement a dual reference counting mechanism, and a pointer type conversion mechanism. While doing this, I found certain aspects in common, so I grouped pointers and counters (and optionally even objects) in common base classes.

3 categories of pointers for 2 x 2 category of objects

It may seem things become a bit complicated, but it isn’t. In general, we’ve to deal with three types of pointers:

  • "dumb" pointers: the standard class* provided by the C++ language: unaware of object management of every kind.
  • "weak" pointers: do not create, maintain or destroy objects, but they become NULL if what they refer is "released" by a "strong" pointer. Good for back-reference !
  • "strong" pointers: increment and decrement ref-counters when acquiring or releasing objects, deleting the referred objects when his "strong" counter goes to zero.

And we can classify the objects into two categories:

  • "weak" objects: unaware that they are ref-counted.
  • "strong" objects: aware that ref-counting exists.

This can help to solve certain “conversion” problems and again, we can reclassify the objects into two other categories:

  • "simple" objects: all the meaning of the object is in the object itself
  • "aggregated" objects: the meaning of an object is distributed in a group of components

1st question: where to put ref-counters?

I found three answers:

  1. In the objects themselves: it is safer, but requires you design an object for this purpose. If you don’t, or you can’t … this is not the answer
  2. In a map, indexed by the referred object addresses. Solves the 1st problem, but what happens to performance, when the map grows and grows? And with aggregation?
  3. In a separate dedicated object. Sounds good, but we must take care of conversions, especially for aggregated object: there is the risk to originate different ref-counters for different components of the same entity.

I considered the risk of 3) compared with 2) and the constraints of 1), and I decided to implement 3) with a chance to 1) if needed.

So ref-counting is implemented in a dedicated template class containing:

  • The counter for "strong pointers" (when it goes to zero, the referred object is deleted)
  • The counter for "weak pointers" (when it goes to zero, the ref-count itself can be deleted)
  • A "dumb" pointer to the referred … "aggregator", that is "the object whose life defines the life for all its components".
template<class R>
struct RefCount: public RefCount_base
{
private:
        LONG _cntStrong;
        LONG _cntWeak;
        R* _ptr;
...
};

Because the counter functions do not depend on R, I found convenient in implementation, to derive all the templates from a single common abstract base (Refcount_base) defining all reference counting functions as “pure virtual” (in fact it’s more an “interface” than a “struct”)

Note that both RefCount and its base are put into an unnamed namespace: you cannot use them directly. Note also that increment and decrement are done using the W32 API InterlockedIncrement and InterlockedDecrement. You can replace this with a simple ++ and –- but you may have trouble with multithreading. (In fact, apart from this, I didn’t address the multithreading problem, leaving it for future.)

2nd question: what about aggregation?

I’m not referring specifically to the “COM” concept of “aggregate”, but in a situation where an object in based by a set of other objects.

This is typical in class inheritance or in members embedding. Consider this:

C derives from A and B. You create C through a strong pointer, than you pass it to a strong A pointer. You leave out C from it’s original pointer. (The pointer to A is still in place.)

  1. Leaving the C strong pointer must not destroy C (or you will destroy also A, but it is still referred!)
  2. Leaving A, now, must destroy the whole C, not only A, or bye-bye memory integrity. This can be done using virtual destructor, but if you didn’t design A and C (you are just using them), may be they have no virtual destructor and you are required to not modify them.

For those reasons, smart pointers refer to two entities: the object they represent (may be a subcomponent) and the ref-counter that tracks it. And ref-counter refers to the aggregator object. Or … uhmmm … in fact: the object for which the ref-counter had been instantiated. (This may lead to some nasty bugs, but we can come out, don’t worry!)

template<class T>
class _PtrSmart: public _PtrSmart_base
{
    friend class PtrStrong;
    friend class PtrWeak;
protected:
    T* _pT; //the referenced object;
            //NOTE: it may be different that the
                    //referred by RefCount,
        //because it may be a sub or super object
    RefCount_base* _pRefCnt;
};

Note: _PtrSmart is the base for both PtrStrong<T> and PtrWeak<T> pointers. The difference is the way they manage counters, not the way they refer objects.

Assignment between smart pointers, converts the type they refer, but copies the reference to the ref-counter, making this possible:

class A
{
public:
    A() { std::cout    << "Construct A\n";}
    ~A(){ std::cout <<  "Destruct A\n";};
    void Hello() {  std::cout << "Hello from A\n";}
    virtual  Virt() {std::cout << "Virt from A\n";}
    bool operator<(const A&    a)const {returnfalse;}
    };
class C: public    A
{
public:
    C() {std::cout    << "Construct C\n";}
    ~C()  { std::cout << "Destruct C\n";}
    void Hello() { std::cout << "Hello  from C\n";}
    virtual    Virt() {std::cout <<  "Virt from C\n";}
};


int _tmain(int argc, _TCHAR* argv[])
{
    GE::Safe::PtrStrong<A> hA;
    GE::Safe::PtrStrong<C> hC(new C);
    hC->Hello();
    hA = hC;
    hC = (C*)NULL;
    hA->Hello();
    hA->Virt();
    return 0;
}

When doing hA = hC, the C* in hC becomes a A* in hA, but hA uses the same ref-count object of hC (and increments).

When hC gets null, it decrements, hence, the counter goes to one. So hA is still valid and A still alive.

Finally, when hA goes out of scope (and is destroyed) it decrements the counter. Now the counter is zero, and so the counter object (not the smart pointer):

  • Increments its “weak” counter to keep itself alive (this was a bug fix)
  • Deletes its referred aggregator, that is ... C (and hence, but not only, A)
  • Decrements its “weak pointer”. If no more “weak pointer”s are around, the ref-count object also can suicide (delete this)

Limitations

Sounds good, but there are some limitations:

What really converts C* into A*? Only the fact that C “is an” A? And what about to convert A* to C*? And if C derives from A and B, why don’t convert B* to A*, if referred to instances of B and A as bases of a same C? Will dynamic-cast be a solution?

And if A and B are not “base” but “members” of C? Why not do the same again?

(uhmm .. it’s my impression, or is it really semantically the same of a COM QueryInterface?)

And then: when a “dumb” pointer is assigned to a “smart” pointer, how can the smart pointer properly know what is the ref-counter eventually already associated to the object (it is assigned via “dumb” pointer, but another smart pointer somewhere else may already have created it), without the risk to create a new one, with the result to have two independent counters to the same object both with a same right to kill the object? (C++ can even kill twice, but your memory may not like it!)

All this problems are addressed in the following arguments: "Conversions" and "Object Strength"

Conversions

When a smart pointer is assigned to another, the receiving pointer must receive a reference to the ref-count object the assigning pointer is using, and must convert the type referred by the originating pointer to the type it points.

The mother of all this conversion is the template function T* smart_cast<T,U,C>(U*).

The third parameter is a functional object (a class implementing operator()) I called “caster”.

template<class T, class U>
T* smart_cast(U* pU)
{
    T* pT = NULL;
    
    //try using the function
    pT = FPtrConvertFn<T,U>(pU);
    if(pT)
            return pT;       //successful conversion
    
    //try using functional
    PtrConvert<U>()(pT, pU);
    if(pT) 
            return pT; //successfull conversion

    return C()(pU);
};

In fact, what smart_cast does is try to find a way to convert.

It first tries with a template function, then through template operator() onto template classes. To construct a PtrConvert<U> (U is the "from" type) call its operator()<T>(T*&, U*). The default implementation of this template member of template function simply sets T* to NULL (failed conversion).

If you are in a situation like:

class A { ... };
class B { ... };
 
class C
{
public:
         A m_a;
         B m_b;
};

You can easily specialize a:

class PtrConvert<C>
{
    void operator()<A>(A*& pA, C* pC) { pA = &pC->m_a;}
    void operator()<B>(B*& pB, C* pC) { pB = &pC->m_b;}
};

If you don’t (or let a particular type to the default), smart_cast will try also with FPtrConvertFn<T,U>.

It’s not a functional object. Just an ordinary template function, you can specialize for a particular type pair. For example in our case:

C* FPtrConvert<C,A>(A* pA)
{
         return (C*)(((BYTE*)pA)-ofsetof(C,m_a));
}
 
C* FPtrConvert<C,B>(B* pB)
{
         return (C*)(((BYTE*)pB)-ofsetof(C,m_b));
}

The default still returns NULL (failed).

At this point, if no T* has till been found, smart_cast tries the ax by calling C()(pU), where C is the third template parameter.

This template parameter is taken from the second parameter of both strong and weak pointers, and the default value is FDynamicCast<T>.

In my header I defined it, and I also define a FStaticCast, in the following way:

template<class T>
struct FDynamicCast
{
        template<class U>
        T operator() (U u) {return dynamic_cast<T>(u);}
};

template<class T>
struct FStaticCast
{
        template<class U>
        T operator() (U u) {return static_cast<T>(u);}
};

Of course, they will never work with the previous example, but may be good defaults in case of derivation, where is:

class C:
         public A,
         public B
{
         ...
};

Enabling RTTI (/GR option of C++ compiler, or Language section of project property page, “Enable Run Time Type Information), a default to dynamic-cast is always working consistently.

The only drawback is, it doesn’t work if nothing of virtual exist in A, B and C (non-polymorphic types). In this case static_cast must be used (I provided FStaticCast just for those cases). But, in this case, there is another drawback: you cannot convert A* to B* or vice versa (The fact they are or not both into a same C object depends on the object instance, not on the definition). Thus, a FPtrConvert specialization may be required.

Object "Strength"

There is still an open problem: the assignment of a “dumb” pointer to a "smart" one.

The receiving pointer has no way to know about the ref-counter, because the "dumb" pointer doesn’t know about it.

The only thing it can do is – because it needs one- create a new one.

This is a completely unsafe operation. If other smart pointers are already in place pointing to that object you are left alone in an inconsistent environment. For this reason, this kind of assignment, in a completely safe application should be avoided, and all pointers should be smart, and new objects should be created with the New(...) function(s) of the smart pointers themselves. This assures that object and ref-counters are always one-to-one.

But there is another possibility: put the knowledge about the ref-counting into the objects themselves. By doing so, the ref-counter can be identified even through a dumb pointer.

This is what ObjStrong does.

ObjStrong, in fact, is a class designed to be the base for every object wishing to be “strong”. It carries a signature (see later) and derives virtually from ObjStrong_base (that is internal to the header).

Why virtually? Because if A and B are both "Strong" (hence: derive from ObjStrong) and C derives from A and B (and even ObjStrong again), we must be sure that only one ObjStrong_base exists: why? Because it is – in fact – the creator and destructor for the ref-counter!

So, when a smart pointer receives a dumb pointer, and needs a ref-counter object, it calls the local helper GetRefCountBase. This function attempts to verify if the referred object ... IsSmart , by reinterpreting the pointer (C-style cast) and verifying the smart-object signature is present. If it is, the cast is correct, and the ref-counter is retrieved.

If it isn’t, a new ref-counter is created. Sorry.

Conclusion

I found this implementation very flexible in a variety of situations, and I used it in various applications (MFC or not).

The only thing to take care in MFC are objects whose creation and deletion is managed internally by MFC (like Document, Frame or Views). You cannot use smart pointers with those objects apart the cases where you intercept and - in fact - disable, MFC creation / deletion mechanisms.

Serialization is also a problem: in most of the cases serializing "dumb pointers" obtained by smart pointers and deserializing into "dumb" pointer to assign to smart pointers works. But serialization is a more general problem, I promise to afford in another article.

Update V1.1

when "Delete" is not "delete"

During a discussion with a colleague, he gave me an important suggestion: in my implementation, when a strong counter decrements to zero, the referred object is simply deleted.

This works in most of the cases, but there are cases where an object may not like this.

There are objects that don't like to be deleted, instead, they like to delete themselves after a deletion function (may perform some cleanup or delay deletion after a particular event) has been called.

For this reason, I decided to add another template "functor" (GE_::Safe::FDeletor<T>) whose operator()(T* pT) by default just does delete pT, and modifies the delete _ptr in RefCount::ReleaseStrong with a call to this functor (FDeletor<R>()(_ptr)).

Normally nothing changes, but now, you can specialize that functor for a particular type to do something else than “delete”.

The class that "functor" will refer is the class for which the Refcount has been created. Normally this is:

  • for ObjStrong derived objects it is the ObjStrong_base virtual base
  • for simple objects (having no bases) it is the object class itself
  • for derived objects it is the class that refers the very first smart pointer assigned with that object

For ObjStrong_base, I added a new virtual function (Destroy) and I specialize the FDeletor to call this function. The default implementation, just calls delete this.

So, for ObjStrong derived, you shouldn't specialize the FDeletor: you must instead override the Destroy function.

For complex objects you cannot (or don't want to) derive from ObjStrong, I suggest to always assign every "new" instance to a smart pointer of that instance type first (and eventually reassign to a subtype -smart or not- pointer). This assures that the ref-counter object refers always your type and that FDeletor you can specialize is the one for your class.

Brute force "delete"

What happens if an object that is “smart pointed” is “deleted” by a call to "delete" into a “somebody else function” not aware of the existence of smart pointers?

Clearly, this is inconsistent with all what we said up to now: smart pointers cannot know about that action, outside their control.

But there is a case when we can do something: ObjStrong derived objects: they know about reference counting objects.

In this case, we can – in ObjStrong_base destructor, set the "aggregator reference" in the ref counter to NULL.

Because all smart pointers, when dereferencing, always checks this condition, this is enough to make all smart pointers to behave as they’ve been set all together to NULL. Probably not so beautiful (a pointer that becomes NULL if you don’t expect ...) but better than to have invalid pointers!

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)


Written By
Architect
Italy Italy
Born and living in Milan (Italy), I'm an engineer in electronics actually working in the ICT department of an important oil/gas & energy company as responsible for planning and engineering of ICT infrastructures.
Interested in programming since the '70s, today I still define architectures for the ICT, deploying dedicated specific client application for engineering purposes, working with C++, MFC, STL, and recently also C# and D.

Comments and Discussions

 
GeneralMy vote of 1 Pin
Andy Bantly13-Nov-10 5:19
Andy Bantly13-Nov-10 5:19 
GeneralRe: My vote of 1 Pin
Emilio Garavaglia14-Nov-10 2:30
Emilio Garavaglia14-Nov-10 2:30 
GeneralRe point (2) ... Pin
rtybase28-May-08 4:01
rtybase28-May-08 4:01 
GeneralRe: Re point (2) ... Pin
Emilio Garavaglia28-May-08 21:26
Emilio Garavaglia28-May-08 21:26 
GeneralImprovement suggestions Pin
Jerome.D15-Jul-06 9:34
Jerome.D15-Jul-06 9:34 
QuestionCan not compile in VC6 Pin
poney27-Apr-04 15:14
poney27-Apr-04 15:14 
GeneralProblems compiling H - file Pin
Van Overveldt Luc24-Nov-03 2:23
Van Overveldt Luc24-Nov-03 2:23 
GeneralRe: Problems compiling H - file Pin
Emilio Garavaglia24-Nov-03 4:53
Emilio Garavaglia24-Nov-03 4:53 
GeneralMultithreading Pin
[James Pullicino]28-Oct-03 21:39
[James Pullicino]28-Oct-03 21:39 
GeneralRe: Multithreading Pin
Emilio Garavaglia29-Oct-03 5:18
Emilio Garavaglia29-Oct-03 5:18 
GeneralBoost and Modern C++ Design Pin
Jonathan de Halleux6-Oct-03 19:52
Jonathan de Halleux6-Oct-03 19:52 
GeneralRe: Boost and Modern C++ Design Pin
Emilio Garavaglia6-Oct-03 22:04
Emilio Garavaglia6-Oct-03 22:04 
GeneralRe: Boost and Modern C++ Design Pin
unnamed123217-Oct-03 21:11
sussunnamed123217-Oct-03 21:11 
GeneralRe: Boost and Modern C++ Design Pin
Jonathan de Halleux7-Oct-03 21:38
Jonathan de Halleux7-Oct-03 21:38 
GeneralRe: Boost and Modern C++ Design Pin
Emilio Garavaglia7-Oct-03 21:59
Emilio Garavaglia7-Oct-03 21:59 
GeneralRe: Boost and Modern C++ Design Pin
unnamed123219-Oct-03 16:04
sussunnamed123219-Oct-03 16:04 
GeneralRe: Boost and Modern C++ Design Pin
matty_t14-Oct-04 1:55
matty_t14-Oct-04 1:55 
GeneralRe: Boost and Modern C++ Design Pin
Anonymous1-Dec-04 10:47
Anonymous1-Dec-04 10:47 
GeneralRe: Boost and Modern C++ Design [modified] Pin
Harrison H14-Jul-11 8:19
Harrison H14-Jul-11 8:19 
GeneralRe: Boost and Modern C++ Design Pin
Emilio Garavaglia14-Jul-11 10:46
Emilio Garavaglia14-Jul-11 10:46 
GeneralRe: Boost and Modern C++ Design Pin
Coruscant12-May-05 23:33
Coruscant12-May-05 23:33 
GeneralA difficult subject. Pin
WREY5-Oct-03 9:55
WREY5-Oct-03 9:55 

General General    News News    Suggestion Suggestion    Question Question    Bug Bug    Answer Answer    Joke Joke    Praise Praise    Rant Rant    Admin Admin   

Use Ctrl+Left/Right to switch messages, Ctrl+Up/Down to switch threads, Ctrl+Shift+Left/Right to switch pages.