2012-03-27 17 views
9

Imagine que tenemos algún tipo de protocolo con cientos de tipos de mensajes, cada uno de los cuales queremos modelar por una clase C++. Debido a que cada clase debe ser capaz de procesar cada campo de forma automática, una solución natural es simplemente tener una std::tuple con todos los tipos requeridos:¿Cómo diseñar una clase con campos "anotados"?

std::tuple<int, double, char> message; 

print(message); // the usual variadic magic 

todo esto está bien y bien. Sin embargo, ahora quiero dar un nombre a cada campo, y quiero poder usar el nombre cuando me refiera al campo en mi código, así como obtener una representación textual de él. Ingenuamente, o en C, podría haber escrito:

struct Message 
{ 
    int header; 
    double temperature; 
    char flag; 
}; 

De esa manera se pierde la capacidad de procesamiento automagic recursiva de la tupla, pero podemos nombrar cada campo literalmente. En C++, podemos hacer ambas cosas por medio de una enumeración:

struct Message 
{ 
    enum FieldID { header, temperature, flag }; 
    static const char * FieldNames[] = { "header", "temperature", "flag" }; 

    typedef std::tuple<int, double, char> tuple_type; 

    template <FieldID I> 
    typename std::tuple_element<I, tuple_type>::type & get() 
    { return std::get<I>(data); } 

    template <FieldID I> 
    static const char * name() { return FieldNames[I]; } 

    tuple_type data; 
}; 

Ahora puedo decir, Message m; m.get<Message::header>() = 12; etc., y puedo recursivo sobre los campos y hacer que cada imprimir su propio valor prefijado por su propio nombre, etc.


Ahora la pregunta: ¿Cómo puedo autor dicho código de manera eficiente, sin repetición?

Idealmente, quiero ser capaz de decir esto:

START_MESSAGE(Message) 
ADDFIELD(int, header) 
ADDFIELD(double, temperature) 
ADDFIELD(char, flag) 
END_MESSAGE 

¿Hay alguna manera, la combinación de preprocesador, Boost y C++ 11, para lograr algo como esto sin la necesidad de herramientas de generación externos ? (Creo que Boost.Preprocessor llama a esta repetición "horizontal" y "vertical". Necesito "transponer" los datos de campo de alguna manera). La característica clave aquí es que nunca tengo que repetir ninguna información, y que modificar o agregar un campo solo requiere un cambio único.

+1

Para este tipo de problemas, un lenguaje descriptivo simple y un preprocesador encargo generar un archivo de inclusión es mejor a largo plazo. El "archivo fuente" es fácil de mantener e incluso puede ser generado por herramientas externas si el proyecto lo requiere. –

+0

@AlexandreC .: En cualquier etapa, "una herramienta más pequeña" siempre se ve como la mejor respuesta. Pero en el gran esquema de cosas, solo tienes una cosa más que llevar contigo y mantener, documentar, recordar y entrenar a la gente. Tener algo que funcione de la caja definitivamente vale la pena el dolor de configurar algunas macros horrendas. –

Respuesta

3

Puede hacerlo con las secuencias de preprocesador de boost.

#define CREATE_MESSAGE(NAME, SEQ) ... 

CREATE_MESSAGE(SomeMessage, 
    (int)(header) 
    (double)(temperature) 
    (char)(flag) 
) 

Debería iterar sobre cada par para generar las definiciones. No tengo ningún código de ejemplo a la mano, aunque probablemente pueda arreglar algunos si es interesante.

En un momento tuve un generador para algo como esto que también generó toda la serialización para los campos. Sentí que fue un poco demasiado lejos. Siento que las definiciones concretas y los visitantes declarativos en los campos son más directos. Es un poco menos mágico en caso de que alguien más tenga que mantener el código detrás de mí. No sé cuál es tu situación, obviamente, justo después de implementarlo todavía tenía reservas. :)

Sería genial volver a mirar con las características de C++ 11, aunque no he tenido la oportunidad.

Actualización:

todavía hay pocos problemas de trabajo, pero esto es en su mayoría trabajadores.

#include <boost/preprocessor.hpp> 
#include <boost/preprocessor/seq/for_each_i.hpp> 
#include <boost/preprocessor/arithmetic/mod.hpp> 
#include <boost/preprocessor/control/if.hpp> 

#include <tuple> 

#define PRIV_CR_FIELDS(r, data, i, elem) \ 
    BOOST_PP_IF(BOOST_PP_MOD(i, 2),elem BOOST_PP_COMMA,BOOST_PP_EMPTY)() 

#define PRIV_CR_STRINGS(r, data, i, elem) \ 
    BOOST_PP_IF(BOOST_PP_MOD(i, 2),BOOST_PP_STRINGIZE(elem) BOOST_PP_COMMA,BOOST_P 

#define PRIV_CR_TYPES(r, data, i, elem) \ 
    BOOST_PP_IF(BOOST_PP_MOD(i, 2),BOOST_PP_EMPTY,elem BOOST_PP_COMMA)() 

#define CREATE_MESSAGE(NAME, SEQ) \ 
    struct NAME { \ 
     enum FieldID { \ 
      BOOST_PP_SEQ_FOR_EACH_I(PRIV_CR_FIELDS, _, SEQ) \ 
     }; \ 
     std::tuple< \ 
      BOOST_PP_SEQ_FOR_EACH_I(PRIV_CR_TYPES, _, SEQ) \ 
     > data;\ 
     template <FieldID I> \ 
      auto get() -> decltype(std::get<I>(data)) { \ 
       return std::get<I>(data); \ 
      } \ 
     template <FieldID I> \ 
      static const char * name() { \ 
       static constexpr char *FieldNames[] = { \ 
        BOOST_PP_SEQ_FOR_EACH_I(PRIV_CR_STRINGS, _, SEQ) \ 
       }; \ 
       return FieldNames[I]; \ 
      } \ 
    }; 

CREATE_MESSAGE(foo, 
     (int)(a) 
     (float)(b) 
    ) 

#undef CREATE_MESSAGE 

int main(int argc, char ** argv) { 

    foo f; 
    f.get<foo::a>() = 12; 

    return 0; 
} 

Está teniendo problemas con get's decltype. Realmente no he usado la tupla para saber qué esperar allí. Sin embargo, no creo que tenga nada que ver con la forma de generar los tipos o campos.

Esto es lo que el preprocesador está produciendo con -E:

struct foo { 
    enum FieldID { a , b , }; 
    std::tuple< int , float , > data; 
    template <FieldID I> 
    auto get() -> decltype(std::get<I>(data)) { 
     return std::get<I>(data); 
    } 
    template <FieldID I> static const char * name() { 
    static constexpr char *FieldNames[] = { "a" , "b" , }; 
    return FieldNames[I]; 
    } 
}; 
+0

Eso suena prometedor. Déjame leer la documentación de eso. Si tiene un ejemplo de código más completo, estaría muy agradecido (y tal vez envíe alguna recompensa). –

+0

@KerrekSB Aquí hay una referencia a un enlace (boost vault) para alguien que intenta hacer enums escritos a máquina más fuertes, lo que debería mostrar que al menos harías la iteración sobre la secuencia. http://stackoverflow.com/a/439004/839436 –

+0

@KerrekSB Actualizado. Hasta ahora no está funcionando del todo, pero parece que se están generando los campos de estructura. –

1

Esto no es una respuesta, sino simplemente otra idea (aterradora) a considerar. Tengo un archivo inl que escribí una vez que algo así es vagamente similar. Es aquí: http://ideone.com/6CvgR

El concepto básico es la persona que llama hace esto:

#define BITNAME color 
#define BITTYPES SEPERATOR(Red) SEPERATOR(Green) SEPERATOR(Blue) 
#define BITTYPE unsigned char 
#include "BitField.inl" 

y el archivo inl crea un tipo de campo de bits personalizado con miembros nombrados por la redefinición de SEPERATOR y luego usando BITTYPES nuevo. Que luego se puede usar fácilmente, incluida una función ToString.

colorBitfield Pixel; 
Pixel.BitField = 0; // sets all values to zero; 
Pixel.Green = 1; // activates green; 
std::cout << "Pixel.Bitfield=" << (int)Pixel.BitField << std::endl; //this is machine dependant, probably 2 (010). 
Pixel.BitField |= (colorBitfield::GreenFlag | colorBitfield::BlueFlag); // enables Green and Blue 
std::cout << "BlueFlag=" << (Pixel.BitField & colorBitfield::BlueFlag) << std::endl; // 1, true. 
std::cout << "sizeof(colorBitField)=" << sizeof(colorBitfield) << std::endl; 

El archivo en línea en sí es aterrador código, pero algunos enfoque vagamente como esto podría simplificar el uso de la persona que llama.

Si tengo tiempo después, veré si puedo hacer algo con esta idea para lo que usted quiere.

0

Se podría hacer algo similar a lo que BOOST_SERIALIZATION_NVP (de la biblioteca Boost.Serialization) lo hace. La macro crea una estructura contenedora (efímera) que enlaza el nombre de su argumento y el valor. Este par nombre-valor es procesado por el código de la biblioteca (el nombre en realidad solo es importante en la serialización XML; de lo contrario, se descarta).

Por lo tanto, el código podría ser:

int header  = 42; 
double temperature = 36.6; 
char flag  = '+'; 
print (Message() + MY_NVP (header) + MY_NVP (temperature) + MY_NVP (flag)); 
+0

Hm, interesante ... Me preguntaba si Boost.la serialización puede tener herramientas que puedan ayudar con el trabajo. Pero realmente me gustaría la clase real de 'Message'. Ya tengo un código de serialización para él, así que tal vez solo quiera crear una instancia, completarla y enviarla a mi serializador, por ejemplo, o imprimir su contenido en un archivo de registro. –

+0

@KerrekSB: Creo que te malentendí entonces. ¿Desea mantener la clase 'Message' tal como está ahora * y * también hace posible crear clases similares (' Message1', ... 'Message53')? – doublep

+0

Sí, de hecho. Quiero diseñar muchas clases de mensajes (permanentemente) y quiero tener una manera fácil de * autor * de todas esas clases. Podría simplemente escribir cada uno manualmente como lo hice en el ejemplo, pero sería tedioso y terrible. También podría escribir una herramienta externa para crear las definiciones de clase, pero eso también sería muy insatisfactorio. –

1

Sobre la base de la sugerencia de Tom Kerr, miré hacia arriba secuencias Boost.Preprocessor. Esto es lo que ocurrió:

#include <boost/preprocessor/seq.hpp> 
#include <boost/preprocessor/comma_if.hpp> 
#include <boost/preprocessor/arithmetic.hpp> 
#include <boost/preprocessor/stringize.hpp> 

#include <tuple> 

#define PROJECT1(a,b) a 
#define PROJECT2(a,b) b 

#define BOOST_TT_projectqu(r,data,t) BOOST_PP_COMMA_IF(BOOST_PP_SUB(r, 2)) BOOST_PP_STRINGIZE(PROJECT2 t) 
#define BOOST_TT_project1(r,data,t) BOOST_PP_COMMA_IF(BOOST_PP_SUB(r, 2)) PROJECT1 t 
#define BOOST_TT_project2(r,data,t) BOOST_PP_COMMA_IF(BOOST_PP_SUB(r, 2)) PROJECT2 t 


template <typename T> struct Field { }; 

#define MESSAGE(classname, data) struct classname            \ 
    {                        \ 
     typedef std::tuple<BOOST_PP_SEQ_FOR_EACH(BOOST_TT_project1, ~, data)> tuple_type;   \ 
                           \ 
     static constexpr char const * FieldNames[BOOST_PP_SEQ_SIZE(data)] = { BOOST_PP_SEQ_FOR_EACH(BOOST_TT_projectqu, ~, data) }; \ 
                           \ 
     enum FieldID { BOOST_PP_SEQ_FOR_EACH(BOOST_TT_project2, ~, data) };      \ 
                           \ 
     template <FieldID I> using type = typename std::tuple_element<I, tuple_type>::type;  \ 
                           \ 
     template <FieldID I> typename std::tuple_element<I, tuple_type>::type & get() { return std::get<I>(dat); } \ 
     template <FieldID I> typename std::tuple_element<I, tuple_type>::type const & get() const { return std::get<I>(dat); } \ 
                           \ 
    private:                      \ 
     tuple_type dat;                   \ 
    }; 

MESSAGE(message,   \ 
    ((int, header))   \ 
    ((double,temperature)) \ 
    ((char, flag))   \ 
) 

Compilar toda la cosa con gcc -std=c++11 -E -P (y reformatear) da:

template <typename T> struct Field { }; 

struct message { 
    typedef std::tuple< int , double , char > tuple_type; 
    static constexpr char const * FieldNames[3] = { "header" , "temperature" , "flag" }; 
    enum FieldID { header , temperature , flag }; 
    template <FieldID I> using type = typename std::tuple_element<I, tuple_type>::type; 
    template <FieldID I> typename std::tuple_element<I, tuple_type>::type & get() { return std::get<I>(dat); } 
    template <FieldID I> typename std::tuple_element<I, tuple_type>::type const & get() const { return std::get<I>(dat); } 
    private: tuple_type dat; }; 
Cuestiones relacionadas