Crispy
Crispy

Reputation: 75

C++ storing different pointer types in one map (and handle the destroying)

In my server project I have a connection class which handles one connection to one client. In this connection class I want to store different datas for different systems which aren't defined in the connection class - because the outside should control completely what data is stored. So for example if I want to add a minigame I can just add the data for the minigame (like TMinigameData) to the connection and use it without change anything in the Connection just for this minigame.

My current approach is the following:

    public:
        template <typename T>
        void clear_data()
        {
            auto it = _bind_data.find(typeid(T).hash_code());
            if (it != _bind_data.end())
            {
#ifdef _DEBUG
                delete it->second.second;
#else
                delete it->second;
#endif
                _bind_data.erase(it);
            }
        }

        template <typename T>
        void bind_data(T*&& data)
        {
            bind_data(std::unique_ptr<T>(data));
        }

        template <typename T>
        void bind_data(std::unique_ptr<T>&& data)
        {
            clear_data<T>();

#ifdef _DEBUG
            _bind_data[typeid(T).hash_code()] = std::make_pair(sizeof(T), data.release());
#else
            _bind_data[typeid(T).hash_code()] = data.release();
#endif
        }

        template <typename T>
        T* get_data(bool create_if_not_exists = false)
        {
            auto it = _bind_data.find(typeid(T).hash_code());
            if (it == _bind_data.end())
            {
                if (create_if_not_exists)
                {
                    auto data_ptr = new T();
                    bind_data(std::unique_ptr<T>(data_ptr));
                    return data_ptr;
                }

                return nullptr;
            }

#ifdef _DEBUG
            assert(sizeof(T) == it->second.first, "Trying to get wrong data type from connection");
            return (T*) it->second.second;
#else
            return (T*) it->second;
#endif
        }

    private:
#ifdef _DEBUG
        std::unordered_map<size_t, std::pair<size_t, void*>> _bind_data;
#else
        std::unordered_map<size_t, void*> _bind_data;
#endif

The problem here is the destructor of the different datas won't be called because it's a void pointer. I know the type when adding it into my map but afterwards it gets lost. I don't know how I could store the type / destructor for the specific object.... is my approach generally wrong or what should I do?

Upvotes: 0

Views: 807

Answers (1)

Aconcagua
Aconcagua

Reputation: 25526

Key to a working solution is either virtual inheritance or a custom deleter, as Rinat Veliakhmedov pointed out in his answer already.

However, you cannot use the virtual classes directly, as you'd suffer from object slicing or not being able to use arbitrary types within the same map.

So you need one level of indirection more. To be able to make the polymorphic approach working, you rely on further pointers to avoid object slicing, something like

std::unordered_map<size_t, std::pair<void*, std::unique_ptr<BaseDeleter>>>
std::unordered_map<size_t, std::unique_ptr<void*, std::unique_ptr<BaseDeleter>>>

In first case, you now need to implement all the correct deletion outside the map, second case doesn't work at all as a std::unique_ptr cannot serve as a custom deleter. In both cases, best you can do is wrapping the entire stuff in a separate class, e. g. the polymorphic approach:

class DataKeeper
{
    struct Wrapper
    {
        virtual ~Wrapper() { }
    };
    template <typename T>
    struct SpecificWrapper : Wrapper
    {
        SpecificWrapper(T* t) : pointer(t) { }
        std::unique_ptr<T> pointer;
    };
    std::unique_ptr<Wrapper> data;

public:
    DataKeeper()
    { }
    template <typename T>
    DataKeeper(T* t)
        : data(new SpecificWrapper<T>(t))
    { }
    template <typename T>
    DataKeeper(std::unique_ptr<T>&& t)
        : DataKeeper(t.release())
    { }
};

Now we have an easy to use class DataKeeper that hides all the polymorphism stuff away. I personally consider the custom deleter approach even neater; for that, we'll profit from the fact that ordinary functions can be used as custom deleters as well:

class DataKeeper
{
    template <typename T>
    static void doDelete(void* t)
    {
        delete static_cast<T*>(t);
    }

    std::unique_ptr<void, void(*)(void*)> pointer;
    //                         ^ function pointer type

public:
    DataKeeper()
        : pointer(nullptr, nullptr)
    {}
    template <typename T>
    DataKeeper(T* t)
        : pointer(t, &DataKeeper::doDelete<T>)
        //                       ^ instantiate appropriate template function and pass
        //                         as custom deleter to smart pointer constructor
    { }
    template <typename T>
    DataKeeper(std::unique_ptr<T>&& t)
        : DataKeeper(t.release())
    { }
};

You could now try e. g. as follows:

std::unordered_map<size_t, DataKeeper> map;
map[1] = DataKeeper(new int(7));
map.insert(std::pair<size_t, DataKeeper>(2, std::make_unique<double>(10.12)));
map.emplace(3, std::make_unique<std::string>("aconcagua"));

Create a class with some output in the destructor and you'll see that it gets called correctly.

Upvotes: 2

Related Questions