10
\$\begingroup\$

I wanted to have a way to be able to static_cast to the proper derived type pointer of a base class to simulate a virtual call. Here the virtual call is for a reference counting base class:

template<typename TDerived> class ReferenceCounting   

which should call the destructor of the TDerived type if the reference count drops to zero. This class achieves this by static_casting to its derived class:

delete static_cast<TDerived*>(this)

Here we expose the CRTP pattern.

Let's do a quick example how that would work:

Deriving from this class, let's say:

class A : public ReferenceCounting<A>{ ...code of A... }

and using a smart pointer (here an intrusion pointer), e.g. IntrusionPtr<A> p( new A() ) lets the object be reference counted properly. (if you are interested, the code is below, for smart pointers I refere to boost::shared_ptr and boost::intrusive_ptr)

Question for multiple inheritance using CRTP:
The question came up, what to do, if let's say we want to derive a class B from A and would like to use it also with an intrusive pointer, of course we would like that because class A inherits already from ReferenceCounting<A>:

class B: public A { ...code... };

Using now an intrusive pointer naively like IntrusivePtr<B> p(new B()) will TOTALY fail. Why? Because the class ReferenceCounting is instantiated with TDerived=A which will then only call the destructor of A obviously. One idea would to correct this behavior would be: We need a way to inject the proper derived type into ReferenceCounting (here that would be TDerived=B). How to do this?

My answer can be found here: CRTP Multiple Inheritance.

I just give my implemented answer to the problem and try to explain it:

template<typename PossibleDerived = void>
class A : public ReferenceCounting< 
                            typename Select< PossibleDerived,                  
                                             A<PossibleDerived> >::type                          
                                     >

The Select type trait injects the correct Derived type into ReferenceCounting, i.e A<PossibleDerived> if PossibleDerived=void and PossibleDerived in any other case.

With this declaration of A, we prepare this class to be a base of a derived B we want to use in combination with IntrusivePtr:

class B: public A<B> { ...code... };

If we now use a intrusive pointer like

`IntrusivePtr<B> p(new B())` 

The destruction of the allocated resource B works correctly as ReferenceCounting was instantiated with TDerived=B (-> static_cast to B *and deleting it).

I was interested if somebody has used this already in his code, and what improvements and pitfalls can occur by using this technique. The following pro&cons about this method I could think of so far are:

CON: Multiple static polymorphism is possible for this example but with the problem that every class in the middle of the hirarchy needs take a PossibleDerived=void template definition. This makes also code less readable:

IntrusivePtr< A<> > p( new A<>()) // ok but hm... stupid brackets
IntrusivePtr< A<double> > q( new A<double>()) // huch, that is dangerous!, but can be avoided by no using A in this way!! 

PRO: Making an Intrusive pointer from, let's say:

class C : public B{...code...}

would be dangerous as the ReferenceCounting class has TDerived=B and destructor call from ReferenceCounting will not call destructor for C!!! Fortunately this will NOT compile not compile due to a wrong cast inside IntrusionPtr to a pointer of type ReferenceCounting<C> (See in code below: addRef() function). I think this property is actually awesome.

I would really appreciate to hear what you think about this code, and to hear if this kind of thing should be used or is dangerous enough to not be used and rather stick to virtual dispatching.

Code to test is here:

#include <iostream>
#include <type_traits>
using namespace std;

template<typename T> class IntrusivePtr;

template<typename TDerived>
class ReferenceCounting {
public:
    typedef TDerived Derived ;

    ReferenceCounting() : m_ref(0) { }

    // Do not change reference count if an assignment has been done
    ReferenceCounting& operator= (ReferenceCounting const&){ return *this; }

    unsigned long int getRefCount() const{return m_ref;}

protected:
    ~ReferenceCounting() {std::cout <<"RC:DTOR" <<std::endl; }   

private:

    friend  class IntrusivePtr<TDerived>;
    friend  class IntrusivePtr<const TDerived>;

    unsigned long int addRef() const{
        ++m_ref;
        std::cout << "RC::addRef: " << m_ref <<  std::endl;
        return m_ref;
    }

    // NoDelete is for IntrusivePtr<T>().release()!
    template<bool Delete = true>
    void release() const{
        --m_ref;
        std::cout << "RC::release: " <<  m_ref << std::endl;
        if(!m_ref && Delete){
            std::cout << "RC::delete" << std::endl;
            delete static_cast<Derived const *>(this);
        } 
    }

    mutable unsigned long int m_ref; // Mutable to be changeable also for const objects!
};


template<typename T>
class IntrusivePtr {
public:

    using NonConstType = typename std::remove_const<T>::type;

    IntrusivePtr() : m_p(nullptr) { }

    // Explicit constructor from T* , because we want to avoid that this constructor can be used to convert implicitly to IntrusivePtr
    // somewhere in the code which then deletes the resource unexpectetly!
    // In this constructor/destructors we need a static_cast to really be sure if the type T  inherits somehow from ReferenceCounting<T>
    explicit IntrusivePtr(T* p) : m_p(p) {
        if(p) static_cast<const ReferenceCounting<NonConstType> *>(m_p)->addRef();
    }

    IntrusivePtr(const IntrusivePtr & rhs) : m_p(rhs.m_p) {
        if(m_p) static_cast<const ReferenceCounting<NonConstType> *>(m_p)->addRef();
    }

    // Move support (temporaries)
    // Copy construct from temporary
    IntrusivePtr(IntrusivePtr && rhs) : m_p( rhs.m_p ){
        rhs.m_p = 0; // temporary will not invoke reference count because pointer is zero!
    }

    ~IntrusivePtr() {
        if(m_p) static_cast<const ReferenceCounting<NonConstType> *>(m_p)->release();
    }


    // We want to assign the intrusive ptr to this class
    // m_p points to A, rhs->m_p  points to B
    // This means, decrease ref count of current object A, an set m_p=rhs->m_p 
    // and increase ref count of rhs resource. This can by:
    // Copy and swap idiom, call by value to copy the IntrusivePtr (ref count of B increments)
    // swap this resource pointer into the local temporary rhs (only pointer swap) 
    // deleting the local rhs at end of function decrements ref count of initial resource A 
    IntrusivePtr& operator=(IntrusivePtr rhs) {
        rhs.swap(*this); // swaps the resource pointers
        return *this; // delete rhs-> which decrements correctly our initial resource A!
    }

    // Move Assignment (from temporary)
    // Make sure rhs.m_p is zero and as a consequence the destruction of rhs does not invoke release!
    IntrusivePtr & operator=(IntrusivePtr && rhs){
        IntrusivePtr( std::move( rhs ) ).swap(*this);
        return *this;
    }

    // Reset the IntrusivePtr to some other resource B, 
    // meaning decrementing our resource A and setting the new pointer to B
    // and incrementing B
    // Can also take a nullptr!, making it not default argument because avoiding syntax mistakes with release()
    // which does a complete different thing (takes off the IntrusivePtr)
    void reset(T* p) {
        // Make temporary intrusive pointer for *p (incrementing ref count B)
        // swapping pointers with our resource A, and deleting temporary, decrement A
        IntrusivePtr(p).swap(*this);
    }

    // Release a IntrusivePtr from managing the shared resource
    // Decrements ref count of this resource A but without deleting it!
    T* release() {
        static_cast<const ReferenceCounting<NonConstType> *>(m_p)->template release<false>();
        T* p = m_p;
        m_p = nullptr;
        return p;
    }

    // Get the underlying pointer    
    T* get() const { return m_p; }

    // Implicit cast to T*
    operator T*() const { return m_p; } 
    // Implicit cast to T&
    operator T&() const { return *m_p; }

    T* operator->() const { return m_p; }

    void swap(IntrusivePtr& rhs) {
        std::swap(m_p, rhs.m_p);
    }
private:
    T* m_p;
};


// Helper to select the correct Derived type, the one which gets deleted in the ReferencCounting class
template<typename PossibleDerived,typename T>
struct Select{ typedef PossibleDerived type;};

template<typename T>
struct Select<void,T>{ typedef T type; };


// The base class!
template<typename PossibleDerived = void>
class A : public ReferenceCounting< typename Select<PossibleDerived, A<PossibleDerived> >::type >{
    public:

    A(){}
    ~A(){
        std::cout << "A::DTOR: " <<this <<  std::endl;    
    }

    int foo(){return i[10000];}

    int i[10001];
};


// Use this class (because no brackets <> to write, more handy to write code)
class ANoTemplate final : public A<ANoTemplate>{
    public:

    ANoTemplate(){}
    ~ANoTemplate(){
        std::cout << "B::DTOR: " <<this <<  std::endl;    
    }
};

// if we want to derive from this class we need to be very carefull, as
template<typename PossibleDerived = void>
class B : public A< typename Select<PossibleDerived, B<PossibleDerived> >::type >{
    public:
    ~B(){
        std::cout << "B::DTOR: " <<this <<  std::endl;    
    }

};

class AddendumToC{
    public:
    ~AddendumToC(){
        std::cout << "AddendumToC::DTOR " <<this <<  std::endl;        
    }
};

// Do not derive from this class, otherwise the refernce counting base class deletes the wrong class!
class C : public B<C>, public AddendumToC{
    public:
    ~C(){
        std::cout << "C::DTOR: " <<this <<  std::endl;    
    }
};

// DOOO NOT DOO INHERIT from C if you want reference counting with an intrusive pointer!
// In fact: If we have a intrusivePtr onto a D instance, it will not compile as D cannot be cast into ReferenceCounting<D>!!)
class D : public C{};

int main(){



    {
    A<> *a = new A<>();
    IntrusivePtr< A<> > p (a);
    std::cout << "Created IntrusivePtr< A<> > " <<p<< std::endl;
    }

    {
    ANoTemplate *a = new ANoTemplate();
    IntrusivePtr< ANoTemplate > p (a);
    std::cout << "Created Local IntrusivePtr< ANoTemplate >  " <<p<< std::endl;
    }

    {
    B<> *a = new B<>();
    IntrusivePtr< B<> > p (a);
    std::cout << "Created Local IntrusivePtr< B<> > " <<p<< std::endl;
    }

    {
    C *a = new C();
    IntrusivePtr< C > p (a);
    std::cout << "Created Local IntrusivePtr< C > " <<p<< std::endl;
    }

    {
    D *a = new D();
    //IntrusivePtr< D > p (a);
    //std::cout << "Created IntrusivePtr< D > " <<p<< std::endl;
    //std::cout << "TAKE CARE: ONLY C gets deleted! " <<p<< std::endl;
    }



    {
    using A=ANoTemplate;
    A *a = new A();
    IntrusivePtr<A> p (a);
    std::cout << "Create Local " <<p<< std::endl;
    {
        std::cout << "Create IntrPtr" << std::endl;            
            IntrusivePtr<A> p2(p);
            // Copy object
            A c(p2);
            std::cout << "Ref count of copied obj:" << c.getRefCount() << std::endl;

            A b = *a;
            std::cout << "Ref count of copied obj:" << b.getRefCount() << std::endl;
        {
            std::cout << "Create IntrPtr2 from;" << p.get()<< std::endl;            
            IntrusivePtr<A const> p3(p);
            std::cout << "p3 points to " << p3.get() << " with ref count: " << p3->getRefCount()<< std::endl;
            //p1->i[0]=10;
            //p1.release(); p2.release();  // does not delete object
            const A* r =p3.release();
            std::cout << "Ref count of copied obj:" << r->getRefCount() << std::endl;
        }
    }
    }
}
\$\endgroup\$
  • \$\begingroup\$ added compilable test code \$\endgroup\$ – Gabriel Jun 15 '14 at 22:09
  • 1
    \$\begingroup\$ This doesn't look like multiple inheritance to me. There are not multiple direct base classes, which is the definition of multiple inheritance. Not sure what the name is for a >1 level inheritance, though. \$\endgroup\$ – dyp Jun 15 '14 at 23:32
  • \$\begingroup\$ Instead of Select, you could just pass both A<PossibleDerived> and PossibleDerived, e.g. class A : public RefCounting < A<PossibleDerived>, PossibleDerived >. Alternatively, add a typedef to A to get PossibleDerived. \$\endgroup\$ – dyp Jun 15 '14 at 23:35
  • \$\begingroup\$ Please do not remove the [c++] tag, regardless of the standard(s) utilized. It's the general tag that all the C++ reviewers follow, and it's specifically a language tag. There is a limit of 5 tags for a question, so I've removed [c++11] since you already have [c++14]. \$\endgroup\$ – Jamal Jun 23 '14 at 1:53
  • 1
    \$\begingroup\$ @Gabriel I've posted my personal solution to a similar problem as requested. (From the Eigen Mailing List) (codereview.stackexchange.com/q/79906/64667) \$\endgroup\$ – dwwork Feb 8 '15 at 0:49
3
\$\begingroup\$

I have some causes for concern regarding this pattern.

For example:

class C : public B {
    int* ptr;
}

Forgetting to inject the CRTP parameter into a class that requires it will not give a compile error. It may not even give a run-time error. In this example it will just silently give you a memory leak that is a royal pain in the rear end to debug.

This will happily compile and give you a memory leak, even if C inherits properly from B with the CRTP parameters passed around:

IntrusivePtr<B> a(new B()); 
IntrusivePtr<B> b(new C());
a = b;

This basically prevents use of IntrusivePtr with polymorphic classes. But the compiler will happily accept such use and produce code that crashes at best or silently produces erroneous results/leaks memory at worst.

You may remember all the ins and outs of using this class every time but your next colleague may not even think about it and just go:

Cool, an intrusive pointer, I'll use it for this polymorphic code I'm writing.

Why do you not want to use a virtual destructor? This would make all your headaches go away. Yes you need a v-table, yes it's an extra indirection to figure out the correct destructor to call. But unless this is in the 1% of code that is your inner-most time critical loop, it will rarely matter on PC platforms.

\$\endgroup\$
  • \$\begingroup\$ good points; could you tell me where exactly the memory leak occurs? \$\endgroup\$ – Gabriel Apr 20 '15 at 13:54
  • \$\begingroup\$ Could one prohibit the use of IntrusivePtr<B> b(new C()) for making a new intrusive pointer, but allowing to get somekind of IntrusivePtr cast to the middle class b (with some kind of cast_intrusive_ptr<...> is this possible) \$\endgroup\$ – Gabriel Apr 20 '15 at 13:54
  • \$\begingroup\$ IntrusivePtr< B<>::Base >(new B<>{}); This does not compile in the example above, Static Assert (ideone.com/mwVuSH) \$\endgroup\$ – Gabriel Apr 20 '15 at 14:08
  • \$\begingroup\$ Well that's one worry less. But to my eyes the syntax and usage for the user seems complex and error prone. I do not see the benefit of using this over std::shared_ptr and std::unique_ptr. Could you show a potential use case where the above wouldn't suffice? \$\endgroup\$ – Emily L. Apr 20 '15 at 14:57

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.