The problem.
The problem of synchronizing things is that generally it can not be done automatically, the user is usually in charge of specifying how each entity is related; This point of personalization is usually inevitable and making it as comfortable as possible and not prone to mistakes is the tricky part.
Proposal.
When I had to face 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:
template <typename ENUM>
std::map<ENUM, const std::string> nombre_enum{};
template <typename ENUM>
std::map<const std::string, ENUM> valor_enum{};
template <typename ENUM1, typename ENUM2>
std::map<ENUM1, ENUM2> asocia{};
With these template variables declared, we added some initialization functions:
void nombra() {}
template <typename ENUM, typename ... args>
void nombra(const ENUM value, const char *name, args ... tail)
{
nombre_enum<ENUM>.emplace(value, name);
valor_enum<ENUM>.emplace(name, value);
nombra<ENUM>(tail ...);
}
void sincroniza() {}
template <typename ENUM1, typename ENUM2, typename ... args>
void sincroniza(const ENUM1 value1, const ENUM2 value2, args ... tail)
{
asocia<ENUM1, ENUM2>.emplace(value1, value2);
sincroniza(tail ...);
}
With these initialization functions 1 , we must configure the system:
int main()
{
nombra
(
Enum1::A, "Doce",
Enum1::B, "Cuatro",
Enum1::C, "Cinco",
Enum2::NoValue, "Sin valor",
Enum2::A, "Uno",
Enum2::B, "Dos",
Enum2::C, "Tres",
Enum2::MaxValues, "Máximo de Enum2"
);
sincroniza
(
Enum1::A, Enum2::A,
Enum1::B, Enum2::B,
Enum1::C, Enum2::C
);
return 0;
}
And this allows us to change the functions ToString
, FromString
and Convert
in the following way:
template <typename ENUM>
std::string ToString(ENUM valor)
{
std::string result{};
auto found = nombre_enum<ENUM>.find(valor);
if (found != nombre_enum<ENUM>.end()) result = found->second;
return result;
}
template <typename ENUM>
ENUM FromString(std::string const& nombre)
{
ENUM result{};
auto found = valor_enum<ENUM>.find(nombre);
if (found != valor_enum<ENUM>.end()) result = found->second;
else throw std::runtime_error("valor no valido");
return result;
}
template <typename ENUM1, typename ENUM2>
ENUM1 Convert(ENUM2 value)
{
ENUM1 result{};
auto found = asocia<ENUM2, ENUM1>.find(value);
if (found != asocia<ENUM2, ENUM1>.end()) result = found->second;
else throw std::runtime_error("valor no valido");
return result;
}
And consequently:
ToString(Enum1::A); // Devuelve la cadena "Doce".
FromString<Enum1>("Doce"); // Devuelve Enum1::A.
FromString<Enum2>("Doce"); // Lanza una excepcion.
Convert<Enum1>(Enum2::A); // Lanza una excepcion.
Convert<Enum2>(Enum1::A); // Devuelve Enum2::A.
You can see the code working in Wandbox 三 へ (へ ਊ) へ ハ ッ ハ ッ .
Pros and cons.
Pro : Using template variables the compiler itself is in charge of synchronizing the maps nombre_enum
, valor_enum
and asocia
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 the C ++ single definition rule and how this rule works with the templates, according to the C ++ standard (highlighted and translated by me):
3.2 Single definition rule
A definition of a class is exactly required in a translation unit if that class is used so that its type needs to be complete.
[...]
There may be more than one definition of the type of a class (Clause 9), type listed (7.2), online function with external link (7.1.2), class template (Clause 14) , non-static template function (14.5.6), member data of a template class (14.5.1.3), member function of a template class (14.5.1.1), or template specialization for which some template parameters are not specified (14.7) , 14.5.5) in a program where each definition appears in different translation units , [...]
[...]
If D is a template and defined in more than one translation unit, [...], then it will behave as if there was a single definition of D .
Against : The previous point implies that each of the template variables behaves like a global variable and its use in multi-threaded code can be dangerous. But a priori, after the call to nombra
and sincroniza
it is not necessary to write more in the maps, therefore its use through ToString
, FromString
and Convert
would be read only.
Pro : At the time of inserting new values to those listed, the need for changes of 4 (the enumerated and functions ToString
, FromString
and Convert
) has been reduced to 3 (the one listed and the personalization point in nombra
and sincroniza
).
1 In C ++ 17 we can save the empty function of breaking the recursion by using the constant conditional if constexpr
:
template <typename ENUM, typename ... args>
void nombra(const ENUM value, const char *name, args ... tail)
{
nombre_enum<ENUM>.emplace(value, name);
valor_enum<ENUM>.emplace(name, value);
if constexpr (sizeof...(tail) >= 2)
{
nombra<ENUM>(tail ...);
}
}
template <typename ENUM1, typename ENUM2, typename ... args>
void sincroniza(const ENUM1 value1, const ENUM2 value2, args ... tail)
{
asocia<ENUM1, ENUM2>.emplace(value1, value2);
if constexpr (sizeof...(tail) >= 2)
{
sincroniza(tail ...);
}
}