Reputation: 75
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
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