I'm trying to write a relatively simple Settings list structure, but after three different failed design iterations, I'm sure I'm missing a keyword to find the name of my exact problem.
Ideally, I'd like:
- Each setting has a unique Key, for easy de(serialisation)
- Each setting in the list is a class member, or has a named accessor fn, so my IDE can auto-complete
- Each setting can be a different type (String, uint8_t, uint_16_t...)
- Each setting has a
const char*name - The list has a compile-time known size and a random-access-iterator, so I can use array subscripting
Attempt 1
#define MAKE_UNION(struct_name, struct_def) \
struct __dummy_for_size__##struct_name struct_def; \
\
struct struct_name \
{ \
union \
{ \
struct struct_def; \
uint8_t as_list[sizeof(__dummy_for_size__##struct_name)]; \
}; \
};
MAKE_UNION(InputSettings, {
uint8_t source;
uint8_t channel;
bool active;
}) input_settings;
Pros:
- When I type
input_settings.s, IDE can autocomplete toinput_settings.source - Easy array subscripting with
input_settings.as_list[?]
Cons:
- Code feels icky, I might be in undefined behaviour land, with the union. I was even casting to
void*before that... - I'm limited to types of one byte, otherwise my
as_listsubscripting falls apart - No unique key, (de)serialisation is difficult to make backward/forward compatible
- Settings don't have a c-str name
Attempt 2
Most of the implementation is omitted for brevity
struct ISetting{
const std::string unique_key;
/*...*/
};
class SettingList
{
std::vector<std::unique_ptr<ISetting>> list;
};
class ValueSetting : public ISetting {/*...*/};
class TextValueSetting : public ISetting{/*...*/};
SettingList input_settings;
input_settings.list.push_back(TextValueSetting{"SOURCE", {"A", "B"}, 0});
input_settings.list.push_back(ValueSetting{"CHANNEL", 0, 15, 0});
input_settings.list.push_back(ValueSetting{"ACTIVE", 0, 1, 1});
auto get_source_setting = [&]{
return input_settings.list[0];
};
Pros:
- Unique keys make it easy to de(serialise)
- Settings have names
Cons:
- Too complicated
- Probably unnecessary runtime polymorphism. Plus because IRL I'm not using RTTI, I have to bake in the type with an enum and add that to the interface in order to downcast.
- Uses a vector but the size is known
- Settings don't have named accessors. They have to be written and maintained manually (e.g. the
get_source_settinglambda).
Attempt 3
struct Value
{
uint16_t val;
};
struct TextValue
{
uint16_t val;
std::span<std::string_view> values_names;
};
std::array<std::pair<std::string, std::variant<Value, TextValue>>, 3> input_settings
{{
{"SOURCE", TextValue{ /*... */ }},
{"CHANNEL", Value{ /*... */ }},
{"ACTIVE", Value{ /*... */ }},
//...
//...
}};
Pro:
- Unique key
- Random access
- Could abstract away into a class for easy
get_by_key(std::string)
Cons:
- No named accessor
- Non random access iterator
- Wasted memory because of the memory layout of std::variant (minor inconvenience)
Even a get_by_key(std::string) would imply a runtime performance cost. Maybe a compile time get<key_t>() is possible ?
TL;DR:
How can I mix and match different types into a container, and have each element have its own dedicated named getter/setter automatically ? All the elements are known at compile time, I'd like to avoid runtime overhead.