Search code examples
c++polymorphismsmart-pointerspool

Stored-by-Value Pool that support polymorphism, how to use smart pointer?


Introduction

I have a data structure : pool of values. (not pool of pointers)

When I called create(), it will return Handle.

Everything is good so far.

template<class T> class Pool{
    std::vector<T> v;                   //store by value
    Handle<T> create(){  .... }
}
template<class T> class Handle{
    Pool<T>* pool_;                    //pointer back to container
    int pool_index_;                   //where I am in the container
    T* operator->() {                  
        return pool_->v.at(pool_index_);     //i.e. "pool[index]"
    }
    void destroy(){
        pool_-> ... destroy(this)  .... mark "pool_index_" as unused, etc ....
    }
}

Now I want Handle<> to support polymorphism.

Question

Many experts have kindly advised me to use weak_ptr, but I still have been left in blank for a week, don't know how to do it.

The major parts that I stuck are :-

  • Should create() return weak_ptr, not Handle? .... or should Handle encapsulate weak_ptr?

  • If create() return weak_ptr for user's program, ... how weak_ptr would know pool_index_? It doesn't have such field.

  • If the user cast weak_ptr/Handle to a parent class pointer as followed, there are many issues :-

e.g.

class B{}

class C : public B { ......
}
....
{
    Pool<C> cs;
    Handle<C> cPtr=cs.create();
    Handle<B> bPtr=cPtr;     // casting ;expected to be valid, 
                             //       ... but how? (weak_ptr may solve it)
    bPtr->destroy()   ;      // aPtr will invoke Pool<B>::destroy  which is wrong!
                             //     Pool<C>::destroy is the correct one
    bPtr.operator->() ;      // face the same problem as above
}

Assumption

  • Pool is always deleted after Handle (for simplicity).
  • no multi-threading

Here are similar questions, but none are close enough.
C++ object-pool that provides items as smart-pointers that are returned to pool upon deletion
C++11 memory pool design pattern?


Solution

  • Regarding weak_ptr

    A std::weak_ptr is always associated with a std::shared_ptr. To use weak_ptr you would have to manage your objects with shared_ptr. This would mean ownership of your objects can be shared: Anybody can construct a shared_ptr from a weak_ptr and store it somewhere. The pointed-to object will only get deleted when all shared_ptr's are destroyed. The Pool will lose direct control over object deallocation and thus cannot support a public destroy() function.
    With shared ownership things can get really messy.

    This is one reason why std::unique_ptr often is a better alternative for object lifetime management (sadly it doesn't work with weak_ptr). Your Handle::destroy() function also implies that this is not what you want and that the Pool alone should handle the lifetime of its objects.

    However, shared_ptr/weak_ptr are designed for multi-threaded applications. In a single-threaded environment you can get weak_ptr-like functionality (check for valid targets and avoid dangling pointers) without using weak_ptr at all:

    template<class T> class Pool {
        bool isAlive(int index) const { ... }
    }
    
    template<class T> class Handle {
        explicit operator bool() const { return pool_->isAlive(pool_index_); }
    }
    

    Why does this only work in a single-threaded environment?

    Consider this scenario in a multi-threaded program:

    void doSomething(std::weak_ptr<Obj> weak) {
        std::shared_ptr<Obj> shared = weak.lock();
        if(shared) {
            // Another thread might destroy the object right here
            // if we didn't have our own shared_ptr<Obj> 
            shared->doIt(); // And this would crash
        }
    }
    

    In the above case, we have to make sure that the pointed-to object is still accessible after the if(). We therefore construct a shared_ptr that will keep it alive - no matter what.

    In a single-threaded program you don't have to worry about that:

    void doSomething(Handle<Obj> handle) {
        if(handle) {
            // No other threads can interfere
            handle->doIt();
        }
    }
    

    You still have to be careful when dereferencing the handle multiple times. Example:

    void doDamage(Handle<GameUnit> source, Handle<GameUnit> target) {    
        if(source && target) {
            source->invokeAction(target);
            // What if 'target' reflects some damage back and kills 'source'?
            source->payMana(); // Segfault
        }
    }
    

    But with another if(source) you can now easily check if the handle is still valid!

    Casting Handles

    So, the template argument T as in Handle<T> doesn't necessarily match the type of the pool. Maybe you could resolve this with template magic. I can only come up with a solution that uses dynamic dispatch (virtual method calls):

    struct PoolBase {
        virtual void destroy(int index)       = 0;
        virtual void* get(int index)          = 0;
        virtual bool isAlive(int index) const = 0;
    };
    
    template<class T> struct Pool : public PoolBase {
        Handle<T> create() { return Handle<T>(this, nextIndex); }
        void destroy(int index) override { ... }
        void* get(int index) override { ... }
        bool isAlive(int index) const override { ... }
    };
    
    template<class T> struct Handle {
        PoolBase* pool_;
        int pool_index_;
    
        Handle(PoolBase* pool, int index) : pool_(pool), pool_index_(index) {}
    
        // Conversion Constructor
        template<class D> Handle(const Handle<D>& orig) {
            T* Cannot_cast_Handle = (D*)nullptr;
            (void)Cannot_cast_Handle;
    
            pool_ = orig.pool_;
            pool_index_ = orig.pool_index_;
        }
    
        explicit operator bool() const { return pool_->isAlive(pool_index_); }
        T* operator->() { return static_cast<T*>( pool_->get(pool_index_) ); }
        void destroy() { pool_->destroy(pool_index_); }
    };
    

    Usage:

    Pool<Impl> pool;
    Handle<Impl> impl = pool.create();
    
    // Conversions
    Handle<Base> base  = impl; // Works
    Handle<Impl> impl2 = base; // Compile error - which is expected
    

    The lines that check for valid conversions are likely to be optimized out. The check will still happen at compile-time! Trying an invalid conversion will give you an error like this:

    error: invalid conversion from 'Base*' to 'Impl*' [-fpermissive]
    T* Cannot_cast_Handle = (D*)nullptr;

    I uploaded a simple, compilable test case here: http://ideone.com/xeEdj5