This tutorial explains how to setup advanced configs structures and types.
Contents:
Other config structs can be declared a field like any other! Note that the previous config should be declared a config before being used as a sub-config field.
// Declare the first config.
struct ConfigA { ... };
void declare_config(ConfigA& config) { ... }
// Can be used in other configs as regular fields.
struct ConfigB {
int i;
ConfigA config_a;
};
void declare_config(ConfigB& config) {
name("ConfigB");
field(config.i, "i");
field(config.config_a, "config_a");
}
This declaration automatically gets, sets, and checks also the subconfig when ConfigB
is created, decoded, or checked.
ℹ️ Note
By default subconfigs are indexed with a namespace that is identical to their field name. For the above example, if ConfigA has afloat
f
, that would be reflected as:i: value config_a: f: value
This behavior can be overriden using the field
declaration for subconfigs as follows:
field(config.config_a, "config_a", false)
. This would yield:
i: value
f: value
Configs can inherit from other config structs, e.g. to support derived classes whose bases use configs. Simply declare the base type to be a base of a config:
// Declare the base config.
struct ConfigA { float f; };
void declare_config(ConfigA& config) { ... }
// Other configs can derive from
struct ConfigB : public ConfigA { int i; };
void declare_config(ConfigB& config) {
name("ConfigB");
base<ConfigA>(config);
field(config.i, "i");
}
This declaration automatically gets, sets, and checks also the base config when ConfigB
is created, decoded, or checked.
ℹ️ Note
Inheritance uses 'flat' namespaces, e.g. the above example would yieldi: value f: value
✅ Supports
Note thatconfig_utilities
supports all typical inheritance patterns, such assingle
,multiple
,multilevel
, orhierarchical
inheritance, as well asdiamond patterns
. Simply declare all bases andconfig_utilities
will do the rest:// Works! struct DerivedConfig : public ConfigA, ConfigB { ... }; void declare_config(DerivedConfig& config) { name("DerivedConfig"); base<ConfigA>(config); base<ConfigB>(config); }
Virtual configs are structs that store a config needed do create a BaseObject
(explained in more detail in the chapter delayed object creation with virtual configs), where the specific implementation DerivedObjectX
is not known at compile time. To use virtual configs, include virtual_config.h
. Example case:
// Interface or base class to be used.
struct BaseObject {
struct Config { ... };
virtual void doStuff() = 0;
};
// Different implementations
struct DerivedA : public BaseObject { struct Config { ... }; ... };
struct DerivedB : public BaseObject { struct Config { ... }; ... };
// Config for an object that holds a BaseObject:
struct Config {
// This config can hold a DerivedA::Config or DerivedB::Config, which may be completely different objects!
VirtualConfig<BaseObject> config;
}
Virtual configs are treated like any other config! They can e.g. be declared as sub-configs or parsed from data sources and will autotically loaded, printed, and checked as any other config.
ℹ️ Note
Note that virtual configs are 'uninitialized' when constructed, and only create the contained config when they or their parent config is parsed from data.
By default, uninitialized virtual configs are not considered valid:
VirtualConfig<Base> config;
isValid(config); // Will return false with a warning that the config is required but not set.
This behavior can be overriden by marking a virtual config as optional:
VirtualConfig<Base> config;
config.setOptional();
isValid(config); // Will return true as the config is optional.
Similar to uninitialized smart pointers, virtual configs implement operator bool
to check if they are initialized:
if (config) {
// config is initialized and can be used to create the object.
std::unique_ptr<BaseObject> object = config.create();
} else {
// Do something with (optional) config that is uninitialized.
}
Certain types may need special conversions or checks. Such Converter
can be specified as a template argument of the field
call.
ℹ️ Note
For just parsing of different types, they only need to be yaml-serializable. More details on custom parsing and custom conversions can be found in the advanced documentation.
An example use case is parsing of a thread count, where negative values default to the system hardware concurrency.
Such a conversion is implemented in types/conversions.h
and can be called as follows:
// Specifying the converter as template argument.
field<ThreadNumConversion>(config.num_threads, "num_threads");
Another frequent use case is that of parsing enum
values. To use this converter, include types/enum.h
.
The enum converter will parse the enum values to/from human readable string representations and check the values are valid. There are several equivalent ways of declaring the conversion:
enum class MyEnum { kA, kB, kC};
// Declare the enum conversion globally via a static initializer, so it can be converted everywhere:
auto init = Enum<MyEnum>::Initializer({{MyEnum::kA, "A"}, {MyEnum:kB:, "B"}, {MyEnum::kC, "C"}});
// Afterward the enum conversion can be used in the code:
MyEnum enum_field;
std::string enum_str = Enum<MyEnum>::toString(enum_field);
enum_field = Enum<MyEnum>::fromString(enum_str);
// Config fields can now be declared using the converter:
field<Enum<MyEnum>>(config.enum, "enum");
// Alternatively, the parsing can equivalently be specified directly in the field declaration.
// Note: This can also be used to temporarilly override the global definition:
enum_field<MyEnum>(config.enum, "enum",{{MyEnum::kA, "A"}, {MyEnum:kB:, "B"}, {MyEnum::kC, "C"}});
// For sequential enums, this can also equivalently be declared in short form:
enum_field<MyEnum>(config.enum, "enum",{"A", "B", "C"});
Configs can declare sub-namespaces for their parameters when getting/setting their values. Two equivalent interfaces are provided:
void declare_config(MyConfig& config){
field(config.a, "a");
enter_namespace("ns1"); // Enters a new namespace 'ns1'
field(config.b, "b");
exit_namespace(); // Exits the last namespace, here 'ns1'
enter_namespace("ns2");
field(config.c, "c");
enter_namespace("ns3"); // Namespaces can also be nested.
field(config.d, "d");
}
This will result in:
a: value
ns1:
b: value
ns2:
c: value
ns3:
d: value
✅ Supports
For easier use,exit_namespace()
followed byenter_namespace("ns2")
can be replaced byswitch_namespace("n2")
. To exit all open namespaces one can useclear_namespaces()
.
✅ Supports
Recall that subconfig fields by default open a namespace with their field name. Any residual namespace left open in a subconfig will be closed when returning to the original config declaration body.
Equivalently, we provide scoped namespace declarations. The below code will produce the same namespaces:
void declare_config(MyConfig& config){
field(config.a, "a");
{
NameSpace ns("ns1"); // Scoped namespace definition.
field(config.b, "b");
}
NameSpace ns("ns2");
field(config.c, "c");
NameSpace more_ns("ns3");
field(config.d, "d");
}