20

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

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

  • [ 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
Community
  • 1
  • 1

2 Answers2

2

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.

Yakk - Adam Nevraumont
  • 262,606
  • 27
  • 330
  • 524
  • You said, "I am unsure why you think intrusive_ptr will have better performance". It is partially because I am new to `Boost` (I mentioned it! :-)). Another reason I didn't mention is that I am going to write a C++ wrapper to the `JSON-C` library (which already has reference counting), and I'll make `RCObject` the parent of `JsonObject`. I think it may be confusing to mix `smart_ptr` and `intrusive_ptr`, isn't it? – Siu Ching Pong -Asuka Kenji- Mar 18 '13 at 11:23
  • (1) After thinking for a while, it seems that making `RCObject` the parent class of `JsonObject` a mistake, because this duplicates the reference count field. Anyway, I'll find a way to solve this. (2) `unique_ptr` isn't suitable for me, since my objects will be shared (probably by multiple threads, too, but I'd like to overlook multi-threading in this post). (3) For the performance issue, I'll use a memory allocation library optimized for allocating small objects in multiple threads (something like `tcmalloc`). I'll make some classes POD too, and use regular `std::` containers for them. – Siu Ching Pong -Asuka Kenji- Mar 18 '13 at 12:49
  • Have you considered using a different JSON library? There are quite a few. – Sebastian Redl Mar 18 '13 at 16:34
  • @SebastianRedl : Yes, but I will stick to `POSIX` and `OpenGL` (which are C APIs) since I don't even have a choice. – Siu Ching Pong -Asuka Kenji- Mar 18 '13 at 17:16
1
  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.

Kos
  • 70,399
  • 25
  • 169
  • 233
  • 1
    (1) Yes, I only want `RCObject` instances to work with smart pointers. I only want 2 kinds of objects in my engine: PODs and intrusive_ptrs. Clients always have problem dealing with memory management, I would like to do more for them. In short, `RCObject obj;`: not allowed; `RCObject* obj;`: not allowed but no way to ban; `RCObject* obj_list = new RCObject[8];` not allowed; `intrusive_ptr obj;`: recommended; `container< intrusive_ptr >`: recommended; `private: RCObject* m_obj;`: not allowed but no way to ban; `private: intrusive_ptr m_obj;`: recommended. – Siu Ching Pong -Asuka Kenji- Mar 18 '13 at 15:02
  • 1
    (2) By enforcing only PODs and `intrusive_ptr`s, there is little chance leaking memory. Even if the clients do not understand `intrusive_ptr`, they can just follow the idiom by wrapping everything derived from `RCObject` in `intrusive_ptr`. Furthermore, `typedef`s may be considered too: `typedef intrusive_ptr<_RCObject> RCObject;`. – Siu Ching Pong -Asuka Kenji- Mar 18 '13 at 15:18
  • 1
    (3) For the weak reference part: No, I don't want any raw pointers nor weak references (I wish my clients could avoid them too). What I want is to ban raw pointers totally (if I could). However, this seems impossible. In `intrusive_ptr bear1(new Bear)`, `new Bear` returns `Bear*`. One way to do it is to ban constructors completely and only allow factory methods to return `intrusive_ptr`, but it might be too unfriendly for clients to implement `RCObject` subclasses. – Siu Ching Pong -Asuka Kenji- Mar 18 '13 at 15:21
  • 1
    (4) For the factory method part, @cschwan gave the [same suggestion](http://stackoverflow.com/questions/15474684/142239#comment21901759_15474684). I agree that it works when I saw it for the first sight. I will try it later to make sure that it works without "move semantics" / "rvalue references" introduced in C++11. (5) Since I don't know which container is appropriate, I just name those I know (but may be inappropriate). For the baseline, I need a `std::vector`-like container, a `std::set`-like container and a `std::map`-like container for `intrusive_ptr`. – Siu Ching Pong -Asuka Kenji- Mar 18 '13 at 15:30
  • 1
    (6) For the performance part: The framework is designed to be fast and wastes at little memory as possible. I'm afraid `shared_ptr` would destroy this purpose, since it is said that it uses twice as much memory as a `intrusive_ptr` (one pointing to the pointee, one pointing to the counter), which is not acceptable to me. – Siu Ching Pong -Asuka Kenji- Mar 18 '13 at 15:39
  • 1
    (7) Yes, I'm going to do sth similar. I can't make the copy ctor of `RCObject` `private` or otherwise subclasses couldn't use it (or perhaps they don't need it at all?). Since my current implementation looks like `return new Bear(*this);`, which calls the copy ctor of `Bear` (and `RCObject` in turn), I planned to make it `protected`. Ignoring smart pointers, the `clone()` method declared in `RCObject` would be `virtual RCObject* clone() const;` and that declared in `Bear` would be `virtual Bear* clone() const;`. I'm not sure if "covariant return type" still works after adding `intrusive_ptr`. – Siu Ching Pong -Asuka Kenji- Mar 18 '13 at 15:48
  • @AsukaKenji-SiuChingPong- (@2) Please don't do that. Object and handle to object are different, don't confuse people by making one look like the other. – Kos Mar 18 '13 at 16:21
  • @AsukaKenji-SiuChingPong- @(4) Yes, it will work w/o move semantics. There's a temporary smartptr returned (refcount=1), then it may be assigned to a local variable (refcount=2), and then the temporary dies (refcount -= 1, and a delete if there was no assignment). – Kos Mar 18 '13 at 16:24
  • 1
    @AsukaKenji-SiuChingPong- (8) You haven't specified one important thing - when should RCObjects be released? When refcount drops to 0? On demand? Both? – Kos Mar 18 '13 at 16:25
  • 1
    (A) For the `typedef`: No, I don't think it confuses the users. In fact, it should look more natural. For a framework which uses only PODs and smart pointers, everything looks like "passed-by-value". In fact, it really is! The object is passed-by-reference (pointer), but the object pointer itself is passed-by-value. That is why people say that Java only supports passed-by-value, and this is the way how the Java syntax works! – Siu Ching Pong -Asuka Kenji- Mar 18 '13 at 17:30
  • 1
    (B) In Java, you say `Bear b = new Bear();`; In my engine, you say `intrusive_ptr<_Bear> b(new _Bear);`. **please note the _ here and in my original comment** With the `typedef`, you say `Bear b(new _Bear);`. Or, with another `typedef` style used in an example in [`boost::atomic`](http://www.boost.org/doc/html/atomic/usage_examples.html#boost_atomic.usage_examples.example_reference_counters), you say `Bear::ptr_t b(new Bear);`. The 2nd example above may look ugly, but it's only a matter of choosing name that makes you feel comfortable. What about `typedef intrusive_ptr BearPointer;`? – Siu Ching Pong -Asuka Kenji- Mar 18 '13 at 17:37
  • 1
    (C) For the move semantics: Thanks! It's much clearer for me now! You reminded me the fact that the returned object (`intrusive_ptr` in this case) is destroyed **after** the "copy constructor" or the "copy assignment operator" of the "waiting object" is called. What a simple fact I overlooked! And if nobody's waiting on the other side (no assignment), the returned object is destroyed anyway. – Siu Ching Pong -Asuka Kenji- Mar 18 '13 at 17:43
  • 1
    (D) For the releasing: This is a good question! Since `RCObject` is a lower-level building block of the entire engine, it only guarantees a much lower chance of memory leaks. It doesn't care about the big picture - it's the job of the various higher-level "managers". For instance, if a "Switch Scene" call is invoked, the managers shall ensure that all resources related to that scene decrease their refcount by 1 (perhaps by setting the `currentScene` smart pointer pointing to another scene). If the previous `currentScene` is deleted, it propagates the "-1" to its fields as a chain effect. – Siu Ching Pong -Asuka Kenji- Mar 18 '13 at 18:06
  • 1
    (E) Finally, although it sounds like ["Region-based memory management"](http://en.wikipedia.org/wiki/Region-based_memory_management) is more suitable and efficient for the above case (1 scene = 1 zone, reclaim zone when switching scenes), I would still like to try reference counting first. – Siu Ching Pong -Asuka Kenji- Mar 18 '13 at 18:11
  • let us [continue this discussion in chat](http://chat.stackoverflow.com/rooms/26395/discussion-between-kos-and-asuka-kenji-siu-ching-pong) – Kos Mar 18 '13 at 18:44