The Context
I have a toy ECS architecture where I want to serialize and deserialize the whole app state for hot-reloading DLLs. I don’t want the user to have to register all components at the beginning of the application/module init.
That means I have to somewhat infer what component types are used across my application at the beginning of the project. I could use some sort of reflection solution, but I found another solution using C++17 inline static variables and template functions.
My Solution
I realized I could use C++17 inline static variables with inline template functions so all type usages that go through such functions endup registering unique entries in a factory map.
I declare the TypeFactory as such:
//////////////////////////////////////////////
// typeFactory.h
#pragma once
#include <map>
struct TypeFactory
{
using TypeCtorFunc = void*(*)(void);
inline static std::map<const char*, TypeCtorFunc> Registry;
template<typename T>
const inline static auto TypeInitializer = Registry.emplace(T::Name, (TypeCtorFunc)([](){ return (void*)(new T()); }));
template<typename T>
inline static const char* GetTypeId()
{
return (TypeInitializer<T>, T::Name);
}
};
When the application gets initialize it can use the type factory right-away:
//////////////////////////////////////////////
// main.cpp
#include <iostream>
#include "usage.h"
#include "typeFactory.h"
using namespace std;
struct A { inline static const char* Name = "Type<A>"; int a = 0; };
int main(int argc, char** argv) {
cout << "Type Size:" << TypeFactory::Registry.size() << endl;
cout << ((A*)TypeFactory::Registry[A::Name]())->a << endl;
cout << ((B*)TypeFactory::Registry[B::Name]())->b << endl;
cout << endl;
cout << "Type IDs:" << endl;
// Usage in same translation unit
cout << TypeFactory::GetTypeId<A>() << endl;
// Usage in other translation unit
UsageOfB usage;
cout << usage.GetTypeIdForB() << endl;
return 0;
}
Here is an usage of another component’s GetTypeId call in another translation unit:
//////////////////////////////////////////////
// usage.h
#pragma once
struct B { inline static const char* Name = "Type<B>"; int b = 1; };
class UsageOfB {
public:
const char* GetTypeIdForB();
};
//////////////////////////////////////////////
// usage.cpp
#include "usage.h"
#include "typeFactory.h"
const char* UsageOfB::GetTypeIdForB()
{
return TypeFactory::GetTypeId<B>();
}
Console output upon execution is:
Type Size:2
0
1
Type IDs:
Type<A>
Type<B>
The Question
I don’t know the limitations this approach has…
Should I be worried by static initialization order fiasco? Is there a case where this might not fetch all types? Perhaps some TU whose static initialization is delayed from control flow for some reason?
I assume that the order is unreliable and could easily change based on compiler optimization flags, etc.
I also assume that if the program is split into multiple modules (DLLs), each module would endup having it’s own TypeFactory static member instances. If that’s the case, I believe it means that my hot-reloading logic should have some sort of interface to query for types in each each module TypeFactory.
Am I right to assume these things?
I would like to have those answers before I start doing more expensive experimentations with this language feature. So far I have found some links and videos around static inline member variables, but none of those highlight what sort of limitations one would expect in the context of abusing the variables initialization to populate a data-structure like I am doing. From my understanding so far, the Registry and TypeInitializer members would have static storage duration, but I am not 100% sure.
Thanks in advance!