Most of the times I have wanted to load some sort of config file (be it simple key values, JSON, XML, whatever) I tended to have to deal with the type conversions, validations and trying to report useful errors myself and generally this also left a lot of error cases out with people I am working with struggling to understand what they did wrong when things did not quiet work.
I also don't really like the fact that as well as defining the data struct/class itself, I then need this other bit of code that needs to be perfectly synced with it, with all the fields and their data types a second time.
e.g. I may have some code like below.
In the first simpler case, as well as having to implement this template wrapper around the basic file loading lib to get some basic errors, some errors are just not detected, e.g. if the file contained keys/elements I did not expect, they are slightly ignored. And in the second case, that simple logic does not even guarantee that some property was defined at all anywhere in the inheritance chain, leaving it upto the objects constructor to specify useful defaults for everything or post-validation to detect invalid "default" values.
Is there any better way?
In java Ive seen this sort of thing done via attributes and reflection to be able to write a generic loader that can use a Java type's public setters and attributes for extra validation/etc info. But trying to think of ways to do something like that in C++ I just ended up with a mess of magic macro's trying to set up template functions and lambda's without even getting as far as how to best handle embedded collections like VehicleType::weapons.
I know I could manually add things to the below code like checking the list/set of keys/elements in KeyValues against a "expected_members" constant, and for inheritance I could similarly keep track of a "set_members" collection, but that is yet another thing to manually keep track of (e.g. at the very least another copy of all the member names for each object/struct to keep synced).
//Basic without inheritance
//... for all basic types read from config
static const char * ConfigTypeName<bool>::name = "boolean";
//basic version, via std::stringstream since most other solutions seemed to
//either miss error cases or threw exceptions around
//return false on error
template<class T> bool parseString(const std::string &str, T *out);
//Special versions for some types, e.g. "true" and "false" literal strings to bool
bool parseString(const std::string &str, bool *out);
bool parseString(const std::string &str, MyEnum *out);
//templated getter, either for some own simple std::map thing, or wrapping the xml/json/whatever lib
//key is just whatever string was previously used, so can format an error message to throw
//e.g. "'10.5' for 'MyTankgun/Mass' in 'Data/Units/HeavyTank.txt' is not a valid integer"
//Plus lots of overloads/variations to deal with optional items, restrictions like min and max, etc.
template<class T> T KeyValues::get(const std::string &name);
void loadVehicleType(VehicleType *obj, const KeyValues &kv)
{
obj->cost = kv.get<int>("cost", 0, MAX_ITEM_COST);
obj->myfloat = kv.getRanged<float>("myfloat", 1.0f, 1000.0f);//1.0f <= myfloat <= 1000.0f
for (auto weaponKv : kv.getSubList("Weapons"))
{
obj->weapons.push_back(loadWeaponType(weaponKv));
}
}
With inheritance example
void loadVehicleType(VehicleType *obj, const KeyValues &kv)
{
auto extends = kv.get("extends");
if (extends) loadVehicleType(obj, KeyValues(extends));
kv.getOpt<int>("cost", &obj->cost, 0, MAX_ITEM_COST);
kv.getOpt<float>("myfloat", &obj->myfloat, 1.0f, 1000.0f);
auto weapons = kv.getSubList("Weapons");
if (weapons)
{
obj->weapons.clear();
for (auto weaponKv : weapons)
{
...