Greetings!
I wrote a small templated piece of code for containing any data type for my entity framework.
The way it works:
I have a State class which is basically a handle (it contains a vector index) to the actual data.
The actual data is in a vector<T> in a templated class DataStore<T>, which inherits from a basic interface to increase and decrease the reference count for a particulair handle (these are used by State objects to allow easy copying and managing the reference count).
Then there is a DataStoreRepository class, which has a private map<size_t, unique_ptr<IDataStore>>, and a templated method for getting a specific DataStore<T>, which it does by using typeid(T).hash_code() to query the map for existance for a pointer to that data store type (if not exist, it news one into a unique_ptr and puts it in).
The code is below for your perusal, so if i missed explaining something, feel free to ask or see if the code is explanatory enough.
My design goals are:
1. able to put any value type into entities using as much by-value stuff, and as little new'ing of the data as possible (State class)
2. keep away from using global states (DataStoreRepository class)
3. pull the actual data from an object pool (DataStore class)
My questions are:
1. am i doing anything that i really shouldn't be doing?
2. is using typeid in this context okay?
3. can anything here be considered a hack/bad design/code smell?
I'd love it if you could take apart my code and point out anything that seems bad or misguided thanks!
===
I'm most concerned about the concept behind this class. In my hierarchy, the winmain instantiates an Engine class, which itself contains GameContexts, which themselves have one DataStoreRepository each, and when a GameContext is activated, it's DataStoreRepository is bound to State, so every GameConetxt has it's own pool of data. It kinda seems like a hack, but i don't know if i'm just imagining it or if it's justified.
class DataStoreRepository
{
private:
std::unordered_map<size_t, std::unique_ptr<IDataStore>> _storeMap;
public:
template<typename T> DataStore<T>& getDataStore()
{
auto hash = typeid(T).hash_code();
auto it = _storeMap.find(hash);
if(it == _storeMap.end())
{
it = _storeMap.insert(std::make_pair(hash, std::unique_ptr<IDataStore>(new DataStore<T>()))).first;
}
return *static_cast<DataStore<T>*>(it->second.get());
}
};
This is the whole class, and it's relatively simple. I'm thinking of replacing the _freeSlots member to be a set, so that data is always at the start of the _data vector.
class IDataStore
{
public:
virtual ~IDataStore() {}
virtual uint AcquireDataIndex(int index = NOT_FOUND) = 0;
virtual void ReleaseDataIndex(uint index) = 0;
};
template<typename T> class DataStore : public IDataStore
{
private:
typedef std::vector<T> TypeCache;
typedef std::vector<uint> NumberCache;
TypeCache _data;
NumberCache _refs;
NumberCache _freeSlots;
uint _defaultSize;
public:
DataStore(uint size = gDefaultDataStoreSize)
: _defaultSize(size)
{
_data.reserve(size);
_refs.reserve(size);
_freeSlots.reserve(size);
}
uint AcquireDataIndex(int index = NOT_FOUND)
{
if(index == NOT_FOUND)
{
//new data index
if(!_freeSlots.empty())
{
index = _freeSlots.back();
_freeSlots.pop_back();
_data[index] = T();
}
else
{
index = _data.size();
_data.emplace_back();
_refs.push_back(0);
}
}
++_refs[index];
return index;
}
void ReleaseDataIndex(uint index)
{
if(index != NOT_FOUND && _refs[index] != 0)
{
if(--_refs[index] == 0)
{
_freeSlots.push_back(index);
}
}
}
T& getData(uint index)
{
return _data[index];
}
};
Most of the methods in State are non templated, templates here are to make getting the data and setting the data easier. The other methods just use AddRef or Release to point to the same data or get new data.
Before using anything, State::BindStoreRepository is called, and passed in a concrete instance (made on the stack by another class higher up in the call hierarchy, so it's guaranteed to outlive the State objects).
Reason why it's nice to me is because i can do "" State s = 10; "", and it's automatically bound to the right DataStore for the active GameContext, and i can keep them by value in any container i want.
class State
{
private:
static Util::DataStoreRepository* _storeRepo;
Util::IDataStore* _dataStore;
int _dataHandle;
//this checks _dataStore is not nullptr and calls _dataStore->Acquire(_dataHandle);
void AddRef(uint handle = NOT_FOUND);
//this checks _dataStore is not nullptr and calls _dataStore->Release(_dataHandle);
void Release();
public:
static void BindStoreRepository(Util::DataStoreRepository& repo);
State();
State(const State& rhs);
State(State&& rhs);
State& operator=(const State& rhs);
State& operator=(State&& rhs);
~State();
bool isValid();
template<typename T> State(const T& value)
: _dataStore(&_storeRepo->getDataStore<T>()), _dataHandle(_storeRepo->getDataStore<T>().AcquireDataIndex())
{
as<T>() = value;
}
template<typename T> State& operator=(const T& rhs)
{
Util::DataStore<T>& store = _storeRepo->getDataStore<T>();
if(_dataStore != &store)
{
Release();
_dataStore = &store;
AddRef();
}
as<T>() = rhs;
return *this;
}
template<typename T> T& as()
{
Util::DataStore<T>& store = _storeRepo->getDataStore<T>();
if(&store == _dataStore)
{
return store.getData(_dataHandle);
}
throw std::exception("State::as<T>(): Tried to access wrong type!");
}
};