synchronized enumerated

5

Suppose a scenario in which two enumerated must be synchronized:

// 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 useful functions that take advantage of these listed:

// 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 numbered ones in sync?

Ideally, the solution applied would also allow avoiding the cost associated with manually editing utility functions each time values are added / deleted to those listed.

    
asked by eferion 31.03.2017 в 11:31
source

3 answers

5

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 ...);
        }
    }
    
        
    answered by 31.03.2017 / 13:23
    source
    2

    The first step is to create a new file, for example valores.def and move to that file the values listed:

    A = 12,
    B = 4,
    C = 5,
    

    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:

    #ifndef VALOR
    #define VALOR(X,Y)
    #endif
    
    VALOR(A,12)
    VALOR(B,4)
    VALOR(C,5)
    
    #undef VALOR
    

    Now we generate those listed from said file. To do this, simply simply configure the macro at your convenience:

    enum class Enum1
    {
      #define VALOR(X,Y) X=Y,
      #include "valores.def"
    };
    
    enum class Enum2
    {
      NoValue,
      #define VALOR(X,Y) X,
      #include "valores.def"
      MaxValues
    };
    

    And the same for the utility battery:

    std::string ToString(Enum1 valor)
    {
      switch( valor )
      {
        #define VALOR(X,Y) case Enum1::##X: return #X;
        #include "valores.def"
      }
    
      return "";
    }
    
    Enum1 FromString(std::string const& valor)
    {
      #define VALOR(X,Y) if( valor == #X ) return Enum1::##X;
      #include "valores.def"
    
      throw std::runtime_exception("valor no valido");
    }
    
    Enum1 Convert(Enum2 value)
    {
      switch( value )
      {
        #define VALOR(X,Y) case Enum2::##X: return Enum1::##X;
        #include "valores.def"
      }
    
      throw std::runtime_exception("valor no valido");
    }
    

    Advantages of this system:

    • The listed ones can be in different files.
    • Utilities do not require maintenance
    • When using macros, the content of valores.def can be as versatile as we want, being able to contain text, comments, etc ...
    • Supports old C ++ standards
    answered by 06.04.2017 в 11:20
    1

    One option is to use the preprocessor. Modifying your example:

    #define LISTA(X, S) \
    X(A, 12) S \
    X(B, 4) S \
    X(C, 5)
    
    #define COMA ,
    
    // Este enumerado necesita los valores porque se almacenan en la base de datos
    enum class Enum1
    {
    #define X(A, B) A = B
    LISTA(X, COMA)
    #undef X
    };
    
    // Este enumerado es necesario para realizar tareas de iteración
    // y porque si :)
    enum class Enum2
    {
      NoValue,
    #define X(A, B) A,
    LISTA(X, )
    #undef X
      MaxValues
    };
    
    // Convierte cada etiqueta en su representación a string
    std::string ToString(Enum1 valor)
    {
      switch( valor )
      {
    #define X(A, B) case Enum1::A: return #A;
    LISTA(X, )
    #undef X
      }
    
      return "";
    }
    
    Enum1 FromString(std::string const& valor)
    {
    #define X(A, B) if (valor == #A) return Enum1::A;
    LISTA(X, )
    #undef X
    
      throw std::runtime_exception("valor no valido");
    }
    
    Enum1 Convert(Enum2 value)
    {
      switch( value )
      {
    #define X(A, B) case Enum2::A: return Enum1::A;
    LISTA(X, )
    #undef X
      }
    
      throw std::runtime_exception("valor no valido");
    }
    
        
    answered by 04.04.2017 в 17:54