Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Abstract base class for use with smart pointers (intrusive_ptr) - handling inheritance, polymorphism, cloneability and returning from factory methods

Tags:

Requirements

  1. I am writing a class called RCObject, which stands for "Reference Counted Object";
  2. The class RCObject should be abstract, serving as the base class of a framework (EC++3 Item 7);
  3. Creating instances of RCObject subclasses on the stack should be prohibited (MEC++1 Item 27);

    [ ADDED: ]

    [ Assume Bear is a concrete subclass of RCObject ]

    [ C.E. here means Compilation Error ]

    Bear b1;                        // Triggers C.E. (by using MEC++1 Item 27)
    Bear* b2;                       // Not allowed but no way to trigger C.E.
    intrusive_ptr<Bear> b3;         // Recommended
    
    Bear* bs1 = new Bear[8];                   // Triggers C.E.
    container< intrusive_ptr<RCObject> > bs2;  // Recommended
    intrusive_ptr_container<RCObject> bs3;     // Recommended
    
    class SomeClass {
    private:
        Bear m_b1;                 // Triggers C.E.
        Bear* m_b2;                // Not allowed but no way to trigger C.E.
        intrusive_ptr<Bear> m_b3;  // Recommended
    };
    
  4. CLARIFIED: Declaring / returning raw pointers to RCObject (and subclasses) should be prohibited (Unfortunately, I don't think there exists a practical way to enforce it, i.e. triggering compilation errors when the users do not follow). See example source code in Item 3 above;

  5. Instances of RCObject subclasses should be cloneable just like Cloneable in Java. (MEC++1 Item 25);
  6. Users subclassing RCObject should be able to write "Factory Methods" for their subclasses. There should be no memory leak even if the returned value is ignored (not assigned to a variable). A mechanism which is close to this is autorelease in Objective-C;

    [ ADDED: cschwan and Kos pointed out that returning a "smart-pointer-to-RCObject" is sufficient to fulfill the requirement. ]

  7. CLARIFIED: Instances of RCObject subclasses should be able to be contained in an appropriate std:: or boost:: container. I mainly need a "std::vector-like" container, a "std::set-like" container and a "std::map-like" container. The baseline is that

    intrusive_ptr<RCObject> my_bear = v[10];
    

    and

    m["John"] = my_bear;
    

    work as expected;

  8. The source code should be compilable using a C++98 compiler with limited C++11 support (Visual Studio 2008 and gcc 4.6, to be exact).

More Information

  • I am a beginner in Boost (Boost is so big that I need some time to be familiar with it. There may be existing out-of-the-box solutions, but it has a high chance that I am not aware of such a solution);
  • Due to performance considerations, I would like to use intrusive_ptr instead of shared_ptr, but I am open to both of them and even any other suggestions;
  • I don't know whether make_shared(), allocate_shared(), enable_shared_from_this() could help (by the way, enable_shared_from_this() seems not highly promoted in Boost - it could not even be found in the smart pointer main page);
  • I have heard of "writing custom allocators", but I am afraid it is too complicated;
  • I wonder whether RCObject should be inherited from boost::noncopyable privately;
  • I couldn't find any existing implementation that fulfills all of my requirements;
  • The framework is a game engine;
  • The main target platforms are Android and iOS;
  • I know intrusive_ptr_add_ref() and intrusive_ptr_release() and how to implement them using Argument-Dependent Lookup (aka. Koenig Lookup);
  • I know how to use boost::atomic_size_t with boost:intrusive_ptr.

Class Definitions

namespace zoo {
    class RCObject { ... };                  // Abstract
    class Animal : public RCObject { ... };  // Abstract
    class Bear : public Animal { ... };      // Concrete
    class Panda : public Bear { ... };       // Concrete
}

"Non-smart" Version - createAnimal() [Factory Method]

zoo::Animal* createAnimal(bool isFacingExtinction, bool isBlackAndWhite) {
    // I wish I could call result->autorelease() at the end...
    zoo::Animal* result;

    if (isFacingExtinction) {
        if (isBlackAndWhite) {
            result = new Panda;
        } else {
            result = new Bear;
        }
    } else {
        result = 0;
    }

    return result;
}

"Non-smart" Version - main()

int main() {
    // Part 1 - Construction
    zoo::RCObject* object1 = new zoo::Bear;
    zoo::RCObject* object2 = new zoo::Panda;
    zoo::Animal* animal1 = new zoo::Bear;
    zoo::Animal* animal2 = new zoo::Panda;
    zoo::Bear* bear1 = new zoo::Bear;
    zoo::Bear* bear2 = new zoo::Panda;
    //zoo::Panda* panda1 = new zoo::Bear;  // Should fail
    zoo::Panda* panda2 = new zoo::Panda;

    // Creating instances of RCObject on the stack should fail by following
    // the method described in the book MEC++1 Item 27.
    //
    //zoo::Bear b;                         // Should fail
    //zoo::Panda p;                        // Should fail

    // Part 2 - Object Assignment
    *object1 = *animal1;
    *object1 = *bear1;
    *object1 = *bear2;
    //*bear1 = *animal1;                   // Should fail

    // Part 3 - Cloning
    object1 = object2->clone();
    object1 = animal1->clone();
    object1 = animal2->clone();
    //bear1 = animal1->clone();            // Should fail

    return 0;
}

"Smart" Version [Incomplete!]

/* TODO: How to write the Factory Method? What should be returned? */

#include <boost/intrusive_ptr.hpp>

int main() {
    // Part 1 - Construction
    boost::intrusive_ptr<zoo::RCObject> object1(new zoo::Bear);
    boost::intrusive_ptr<zoo::RCObject> object2(new zoo::Panda);
    /* ... Skip (similar statements) ... */
    //boost::intrusive_ptr<zoo::Panda> panda1(new zoo::Bear); // Should fail
    boost::intrusive_ptr<zoo::Panda> panda2(new zoo::Panda);

    // Creating instances of RCObject on the stack should fail by following
    // the method described in the book MEC++1 Item 27. Unfortunately, there
    // doesn't exist a way to ban the user from declaring a raw pointer to
    // RCObject (and subclasses), all it relies is self discipline...
    //
    //zoo::Bear b;                         // Should fail
    //zoo::Panda p;                        // Should fail
    //zoo::Bear* pb;                       // No way to ban this
    //zoo::Panda* pp;                      // No way to ban this

    // Part 2 - Object Assignment
    /* ... Skip (exactly the same as "non-smart") ... */

    // Part 3 - Cloning
    /* TODO: How to write this? */

    return 0;
}

The above code ("Smart Version") shows the expected usage pattern. I am not sure whether this usage pattern follows best practices of using smart pointers or not. Please correct me if it doesn't.

Similar Questions

  • make shared_ptr not use delete (The accepted answer looks elegant !!! Is it some kind of "custom deallocator"s? I am not sure how it compares with intrusive_ptr in terms of time and space efficiency)

  • intrusive_ptr in c++11 (The accepted answer mentioned make_shared() and enable_shared_from_this(), but I do not understand the "but that doesn't allow you to manage the type using different smart pointer types" part)

  • Is there a way to increase the efficiency of shared_ptr by storing the reference count inside the controlled object? (Still digesting)

  • intrusive_ptr: Why isn't a common base class provided? (The answers are not detailed enough)

  • Is this a valid use of intrusive_ptr? (I learnt something from it, but the focus of this question was on "passing a raw pointer to a function which accepts a smart pointer")

  • Reference counting with a generic intrusive pointer client (This one used "CRTP", but I'm afraid further subclassing will make me headache - should I make zoo::Panda extending from zoo::Bear alone, or should I make it extending both zoo::Bear and intrusive_base<zoo::Panda>?)

  • Embedded reference count with Boost shared_ptr (The accepted answer mentioned that while std::enable_shared_from_this() should be okay, boost::enable_shared_from_this() seems to have some problems)

References

  • [ EC++3 ]: Effective C++: 55 Specific Ways to Improve Your Programs and Designs (3rd Edition) by Scott Meyers
    • Item 7: Declare destructors virtual in polymorphic base classes

  • [ MEC++1 ]: More Effective C++: 35 New Ways to Improve Your Programs and Designs (1st Edition) by Scott Meyers
    • Item 25: Virtualizing constructors and non-member functions
    • Item 27: Requiring or prohibiting heap-based objects.

Articles

  • [ atomic ]: Usage examples of boost::atomic - Boost.org
    • [ section ]: Reference counting

  • [ CP8394 ]: Smart Pointers to boost your code - CodeProject
    • [ section ]: intrusive_ptr - lightweight shared pointer

  • [ DrDobbs ]: A Base Class for Intrusively Reference-Counted Objects in C++ - Dr. Dobb's
    • [ page1 ]: Page 1
    • [ page2 ]: Page 2
    • [ page3 ]: Page 3
like image 825
Siu Ching Pong -Asuka Kenji- Avatar asked Mar 18 '13 10:03

Siu Ching Pong -Asuka Kenji-


2 Answers

make_shared creates an instance of your class in the same allocation block as the reference counter. I am unsure why you think intrusive_ptr will have better performance: it is great when there is already reference counting machinery you cannot remove, but this is not the case here.

For clone, I would implement it as a free function that takes a smart pojnter and returns same. It is a friend, and calls a private pure virtual clone method in base that returns a shared pointer to base, then does a fast smart pointer cast to shared pointer to derived. If you prefer clone as a method, use crtp to duplicate this (giving the private clone a name like secret_clone). This gives you covariant smart pointer return types with little overhead.

Crtp with a range of base classes often has you pass both the base and derived classes in. The crtp class derives from base and has the usual self() that returns derived.

Factory functions should return the smart pointer. You can use the custom deleter trick to get a pre-destroy methid call for last cleanup.

If you are completely paranoid, you can block most ways to get a raw pointer or reference to your class: block operator* on the smart pointer. Then the only route to the raw class is an explicit call to operator->.

Another approach to consider is unique_ptr and references to same. Do you need shared ownership and lifetime management? It does make some problems simpler (shared ownership).

Note that dangling weak pointers prevent the memory from make shared from bring recycled.

A serious downside to always using smart pointers is that you cannot have stack instances or instances directly inside containers. Both of these can be serious performance gains.

like image 192
Yakk - Adam Nevraumont Avatar answered Sep 20 '22 03:09

Yakk - Adam Nevraumont


  1. Creating instances of RCObject subclasses on the stack should be prohibited ([MEC++1][mec++1] Item 27);

What's your rationale? MEC++ gives the example of "objects being able to commit suicide", which may make sense in the context of a game framework. Is that the case?

It should be possible to do with a smart enough smart pointer, if you insist on avoiding a simpler workaround.

Note that if that's the case, you'd probably want to also disallow creating arrays of such objects on the stack using new[] - this also prevents from deleting a single one. You'll probably also want to disallow using RCObjects as sub-objects (members in other classes). This would mean that you're disallowing using RCObject values altogether and let client code only handle them through smart pointers.

  1. Declaring / returning raw pointers to RCObject (and subclasses) should be avoided (Unfortunately, I don't think there exists a way to enforce it by issuing compilation errors);

Then you're bound to have weak pointers to have a way of saying "I'm interested in this object but I'm not keeping it alive".

  1. Users subclassing RCObject should be able to write ["Factory Methods"][factory_method] for their subclasses. There should be no memory leak even if the returned value is ignored (not assigned to a variable).

Such function would return a temporary smart pointer object with reference count equal to 1. If this temporary isn't used to initialise another (thus further incrementing the ref count), it will clean the object up. You're safe.

  1. Instances of RCObject subclasses should be able to be contained in a std:: or boost:: container (or whatever is appropriate). I mainly need something similar to std::vector, std::set and std::map;

This kind of disagrees with (3). If you insist on the objects having to be created individually on the heap and passed around through smart pointers (not as values), then you should also use containers of smart pointers.

  • Due to performance considerations, I would like to use [intrusive_ptr][intrusive_ptr] instead of [shared_ptr][shared_ptr], but I am open to both of them and even any other suggestions;

Aren't you optimising prematurely?

Also, I believe that using intrusive pointers takes away the possibility of using weak references - which you're very likely to need, as I mentioned before.

  • I wonder whether RCObject should be inherited from [boost::noncopyable][noncopyable] privately;

If you're disallowing variables of value type and providing a virtual Clone, then there's probably no need for a public copy constructor. You might make a private copy ctor and use it when defining Clone.

like image 37
Kos Avatar answered Sep 21 '22 03:09

Kos