Let's assume a scenario where two enums need to be in sync:
// Este enumerado necesita los valores porque se almacenan en la base de datos
enum class Enum1
{
A = 12,
B = 4,
C = 5,
};
// Este enumerado es necesario para realizar tareas de iteración
// y porque si :)
enum class Enum2
{
NoValue,
A,
B,
C,
MaxValues
};
In addition, we find a catalog of utility functions that take advantage of these enumerated:
// Convierte cada etiqueta en su representación a string
std::string ToString(Enum1 valor)
{
switch( valor )
{
case Enum1::A: return "A";
case Enum1::B: return "B";
case Enum1::C: return "C";
}
return "";
}
Enum1 FromString(std::string const& valor)
{
if( valor == "A" ) return Enum1::A;
if( valor == "B" ) return Enum1::B;
if( valor == "C" ) return Enum1::C;
throw std::runtime_exception("valor no valido");
}
Enum1 Convert(Enum2 value)
{
switch( value )
{
case Enum2::A: return Enum1::A;
case Enum2::B: return Enum1::B;
case Enum2::C: return Enum1::C;
}
throw std::runtime_exception("valor no valido");
}
// ...
Is there a more elegant way to keep the enums in sync?
Ideally, the implemented solution would also avoid the cost associated with manually editing utility functions every time values are added/removed to enumerations.
The problem.
The problem with synchronizing things is that it generally cannot be done automatically, the user is usually in charge of specifying how each entity is related; this point of customization is usually unavoidable and making it as comfortable as possible and not error-prone is the tricky part.
Proposal.
When faced with a similar problem I used variadic templates (C++11) and template variables (C++14). We start by generating a map that associates the types, I use template variables:
With these template variables declared, we add some initialization functions:
With these initialization functions 1 , we must configure the system:
And this allows us to change the functions
ToString
,FromString
andConvert
as follows:And in consequence:
You can see the code working in Wandbox 三へ( へ՞ਊ ՞)へ ハッハッ.
Pros and cons.
Pro : Using template variables the compiler itself takes care of synchronizing the maps
nombre_enum
,valor_enum
andasocia
between each translation unit . In fact, there will be only one copy for each type or combination of types used in the template, this is due to C++'s single definition rule and how this rule works with templates, according to the C++ standard (highlight and translation mine) :Con : The previous point implies that each of the template variables behaves like a global variable and its use in multithreaded code can be dangerous. But a priori, after the call to
nombra
andsincroniza
there is no need to write any more to the maps, so use them viaToString
,FromString
andConvert
it would be read-only.Pro : At the time of inserting new values to the enumerations, the need for changes has been reduced from 4 (the enumeration and the functions
ToString
,FromString
andConvert
) to 3 (the enumeration and the customization point innombra
andsincroniza
).1 In C++17 we can save the empty recursion break function by using the constant conditional
if constexpr
:The first step is to create a new file, for example,
valores.def
and we move the enumerated values to said file:Now we edit that file to enclose each value in a macro. To avoid future errors, we define a base implementation of said macro and its subsequent cleaning:
Now we generate the enumerations from said file. To do this, simply configure the macro at your convenience:
And the same for the utility battery:
Advantages of this system:
valores.def
can be as versatile as we want, being able to contain text, comments, etc...One option is to use the preprocessor. Modifying your example: