2009-03-23 9 views
12

Supongamos que tengo una estructura como esta:¿Se pueden usar plantillas para acceder a las variables de estructura por nombre?

struct my_struct 
{ 
    int a; 
    int b; 
} 

Tengo una función que debería establecer un nuevo valor para cualquiera de los dos "a" o "b". Esta función también requiere especificar qué variable establecer. Un ejemplo típico sería así:

void f(int which, my_struct* s, int new_value) 
{ 
    if(which == 0) 
    s->a = new_value; 
    else 
    s->b = new_value; 
} 

Por razones que no voy a escribir aquí no puede pasar el puntero para a/b para f. Por lo tanto, no puedo llamar a f con la dirección de my_struct :: a o my_struct :: b. Otra cosa que no puedo hacer es declarar un vector (int vars [2]) dentro de my_struct y pasar un entero como índice a f. Básicamente en f Necesito acceder a las variables por nombre.

El problema con el ejemplo anterior es que en el futuro planeo agregar más variables a struct y en ese caso recordaré agregar más sentencias if a f, lo cual es malo para la portabilidad. Una cosa que podría hacer es escribir f como una macro, como esto:

#define FUNC(which) 
void f(my_struct* s, int new_value) \ 
{ \ 
     s->which = new_value; \ 
} 

y luego me podría llamar FUNC (a) o FUNC (b).

Esto funcionaría, pero no me gusta usar macros. Entonces mi pregunta es: ¿hay alguna manera de lograr el mismo objetivo usando plantillas en lugar de macros?

EDIT: Trataré de explicar por qué no puedo usar punteros y necesito acceder a la variable por nombre. Básicamente, la estructura contiene el estado de un sistema. Este sistema necesita "deshacer" su estado cuando se lo solicite. Deshacer se maneja mediante una interfaz llamada undo_token así:

class undo_token 
{ 
public: 
    void undo(my_struct* s) = 0; 
}; 

así que no puedo pasar punteros con el método de deshacer causa de polimorfismo (MyStruct contiene variables de otros tipos también).

Al agregar una nueva variable a la estructura I, también es posible añadir una nueva clase, así:

class undo_a : public undo_token 
{ 
    int new_value; 
public: 
    undo_a(int new_value) { this->new_value = new_value; } 
    void undo(my_struct *s) { s->a = new_value} 
}; 

El problema es que no sé puntero a s cuando se crea el token, por lo que no puede guarde un puntero a s :: a en el constructor (que habría resuelto el problema). La clase para "b" es la misma, solo tengo que escribir "s-> b" en lugar de s-> a

Quizás esto sea un problema de diseño: Necesito un token de deshacer por tipo de variable, no uno por variable ...

+0

FUNC (a) - definirá f (s, new_value). ¿Pero cómo lo llamarás entonces? –

+0

Sí, lo siento, fue un error. Pero espero que entiendas el punto de todos modos :) – Emiliano

+0

Incluso después de tu edición, utilizando mi respuesta y la de Mykola Golubyev, aún puedes parametrizar tu clase deshacer con un puntero a un miembro de datos de clase y vincularla a la clase/estructura al invocar miembros hora. – camh

Respuesta

16
#include <iostream> 
#include <ostream> 
#include <string> 

struct my_struct 
{ 
    int a; 
    std::string b; 
}; 

template <typename TObject, typename TMember, typename TValue> 
void set(TObject* object, TMember member, TValue value) 
{ 
    (*object).*member = value; 
} 

class undo_token {}; 

template <class TValue> 
class undo_member : public undo_token 
{ 
    TValue new_value_; 
    typedef TValue my_struct::* TMember; 
    TMember member_; 

public: 
    undo_member(TMember member, TValue new_value): 
     new_value_(new_value), 
     member_(member) 
    {} 

    void undo(my_struct *s) 
    { 
     set(s, member_, new_value_); 
    } 
};  

int main() 
{ 
    my_struct s; 

    set(&s, &my_struct::a, 2); 
    set(&s, &my_struct::b, "hello"); 

    std::cout << "s.a = " << s.a << std::endl; 
    std::cout << "s.b = " << s.b << std::endl; 

    undo_member<int> um1(&my_struct::a, 4); 
    um1.undo(&s); 

    std::cout << "s.a = " << s.a << std::endl; 

    undo_member<std::string> um2(&my_struct::b, "goodbye"); 
    um2.undo(&s); 

    std::cout << "s.b = " << s.b << std::endl; 

    return 0; 
} 
+0

¿Leyó el bit donde dijo que no podía tomar la dirección de a o b? –

+0

¿No quiere decir que no puede pasar cosas como & a una función? –

+0

¡Perfecto! Esto resolvió mi problema. Inteligente Y simple.Gracias – Emiliano

2

No puede usar plantillas para resolver esto, pero ¿por qué usar una estructura en primer lugar? Esto parece un uso ideal para un std :: map que mapearía nombres a valores.

+1

porque "f" es solo una función necesaria para hacer cosas secundarias en la estructura. No quiero que la forma de la estructura se vincule a lo que "f" necesita. – Emiliano

4

No estoy seguro de por qué no puede usar un puntero, así que no sé si es apropiado, pero eche un vistazo a C++: Pointer to class data member, que describe una manera de pasar un puntero a un miembro de datos de una struct/clase que no apunta directamente al miembro, pero que luego se enlaza a un puntero de estructura/clase. (énfasis añadido después de la edición del póster que explica por qué no se puede usar un puntero)

De esta manera no se pasa un puntero al miembro, sino que se parece más a un desplazamiento dentro de un objeto.

3

Suena como lo que estás buscando se llama "reflection", y sí a menudo se implementa con una combinación de plantillas y macros. Tenga en cuenta que las soluciones de reflexión a menudo son complicadas y molestas para trabajar, por lo que es posible que desee investigar un poco antes de sumergirse en el código para averiguar si esto es realmente lo que desea.

Segundo hit en Google para "plantillas de reflexión C++" era un documento sobre "Reflection support by means of template metaprogramming". Eso debería hacerte comenzar. Incluso si no es exactamente lo que estás buscando, puede mostrarte una forma de resolver tu problema.

2

Según lo que describió, supongo que no tiene forma de redefinir la estructura.

Si lo hizo, le sugiero que use Boost.Fusion para describir su estructura con campos con nombre de plantilla. Consulte associative tuples para obtener más información al respecto. Ambos tipos de estructuras podrían ser compatibles (la misma organización en la memoria), pero estoy bastante seguro de que no hay forma de obtener dicha garantía del estándar.

Si no lo hace, puede crear un complemento a la estructura que le daría acceso a los campos de la misma manera que lo hacen las tuplas asociativas. Pero eso puede ser un poco verbal.

EDITAR

Ahora está bastante claro que se pueden definir las estructuras de la forma en que desea. Así que definitivamente te sugiero que uses boost.fusion.

34

Para responder la pregunta exacta, la hay, pero es bastante complicada, y será puramente una cuestión de tiempo de compilación. (Si necesita buscar en tiempo de ejecución, use un puntero-a-miembro y, en función de su pregunta actualizada, puede haber entendido mal cómo funcionan)

Primero, necesita algo que pueda usar para representar el "nombre de un miembro "en tiempo de compilación. En la metaprogramación en tiempo de compilación, todo lo que no sea enteros debe representarse por tipos. Entonces usarás un tipo para representar a un miembro.

Por ejemplo, un miembro de tipo entero que almacena la edad de una persona, y otro para el almacenamiento de su apellido:

struct age { typedef int value_type; }; 
struct last_name { typedef std::string value_type; }; 

Luego hay algo así como un map que hace operaciones de búsqueda en tiempo de compilación. Vamos a llamarlo ctmap. Démosle soporte para hasta 8 miembros. En primer lugar necesitamos un marcador de posición para representar la ausencia de un campo:

struct none { struct value_type {}; }; 

entonces podemos remitir-Declarar la forma de ctmap:

template < 
    class T0 = none, class T1 = none, 
    class T2 = none, class T3 = none, 
    class T4 = none, class T5 = none, 
    class T6 = none, class T7 = none 
    > 
struct ctmap; 

A continuación, se especializan esto para el caso en que no hay campos :

template <> 
struct ctmap< 
    none, none, none, none, 
    none, none, none, none 
    > 
{ 
    void operator[](const int &) {}; 
}; 

La razón de esto será clara (posiblemente) en un momento. Finalmente, la definición para todos los demás casos:

template < 
    class T0, class T1, class T2, class T3, 
    class T4, class T5, class T6, class T7 
    > 
    struct ctmap : public ctmap<T1, T2, T3, T4, T5, T6, T7, none> 
    { 
     typedef ctmap<T1, T2, T3, T4, T5, T6, T7, none> base_type; 

     using base_type::operator[]; 
     typename T0::value_type storage; 

     typename T0::value_type &operator[](const T0 &c) 
     { return storage; } 
}; 

¿Qué demonios está pasando aquí?Si pones:

ctmap<last_name, age> person; 

C++ va a construir un tipo de person mediante la ampliación de las plantillas de forma recursiva, porque ctmaphereda de sí, y proporcionamos almacenamiento para el primer campo y luego descartarlo cuando heredamos. Todo esto viene a una parada repentina cuando no hay más campos, debido a la especialización de todo- none patadas en

Así que podemos decir:.

person[last_name()] = "Smith"; 
person[age()] = 104; 

Es como mirar en un map, pero al tiempo de compilación, usando una clase de denominación de campo como clave.

Esto significa que también podemos hacer esto:

template <class TMember> 
void print_member(ctmap<last_name, age> &person) 
{ 
    std::cout << person[TMember()] << std::endl; 
} 

Esa es una función que imprime el valor de un solo miembro, en el que el miembro de que se imprimirá es un parámetro de tipo. Por lo que podemos llamar de esta manera:

print_member<age>(person); 

Así que sí, se puede escribir una cosa que es un poco como un struct, un poco como un tiempo de compilación map.

+5

No es exactamente la cosa más limpia del mundo, pero inteligente, no obstante. +1 solo por enseñarme algo nuevo. –

+2

Una respuesta muy completa, pero es un exceso de lo que necesito. Como dijo Dan, obtuviste un +1 por enseñarme algo nuevo, de todos modos. – Emiliano

+0

Gracias. Espero que alguien lo encuentre y lo encuentre útil: ¡el título de la pregunta parece funcionar de esa manera! –

1

No puedo pensar en una razón por la que no tendrías todo a mano al crear un comando de deshacer. Lo que quieres poder deshacer, lo has hecho. Así que creo que puede usar punteros para los miembros de la clase e incluso punteros a los campos de una instancia de clase particular al crear el comando Deshacer.

Usted está justo en su sección de EDITAR. Es es una cuestión de diseño.

+0

Porque hay algunos puntos en el código donde el estado del sistema está definido por "puntos de control". Esto se implementa eliminando el estado actual y creando uno nuevo. Es cierto que en el 90% de los casos, el puntero es el mismo, pero este no es siempre el caso. – Emiliano

+0

Si elimina un objeto de estado y crea uno nuevo, ¿por qué no encuentra una manera de no eliminar el estado anterior, pero deshabilitarlo para que, si deshace la eliminación, vuelva a trabajar con el mismo objeto? objeto? Puede que no esté claro, lo siento, pero no estoy seguro de haberlo entendido bien. –

+0

Bueno, supongo que si puedo afirmar que el puntero de estado del sistema siempre es válido, entonces no tiene sentido mi pregunta. Aún así, no estoy seguro de que sea una buena idea modificar el código existente para que se ajuste a "admitir" uno. También vea mi comentario a la respuesta de Neil Butterworth. – Emiliano

6

Mykola Golubyev's answer es bueno, pero se puede mejorar ligeramente usando el hecho de que los punteros a los miembros se pueden utilizar como parámetros de plantilla no de tipo:

#include <iostream> 
#include <ostream> 
#include <string> 

struct my_struct 
{ 
    int a; 
    std::string b; 
}; 

template <typename TObject, typename TMember, typename TValue> 
void set(TObject* object, TMember member, TValue value) 
{ 
    (*object).*member = value; 
} 

class undo_token {}; 

template <class TValue, TValue my_struct::* Member> 
class undo_member : public undo_token 
{ 
     // No longer need to store the pointer-to-member 
     TValue new_value_; 

public: 
     undo_member(TValue new_value): 
       new_value_(new_value) 
     {} 

     void undo(my_struct *s) 
     { 
       set(s, Member, new_value_); 
     } 
};  

int main() 
{ 
    my_struct s; 

    set(&s, &my_struct::a, 2); 
    set(&s, &my_struct::b, "hello"); 

    std::cout << "s.a = " << s.a << std::endl; 
    std::cout << "s.b = " << s.b << std::endl; 

    undo_member<int, &my_struct::a> um1(4); 
    um1.undo(&s); 

    std::cout << "s.a = " << s.a << std::endl; 

    undo_member<std::string, &my_struct::b> um2("goodbye"); 
    um2.undo(&s); 

    std::cout << "s.b = " << s.b << std::endl; 

    return 0; 
} 

Esto afeita el costo de un puntero a miembro de cada instancia de undo_member.

+0

+1. Nunca vi esto. Pero esto creará muchas clases una para cada miembro de la estructura. –

+0

@Mykola: Es cierto, pero ¿es un problema? Todos los métodos estarán escritos por el compilador, por lo que no creo que haya ningún código inflado. –

+0

No estarán en línea, ya que en la pregunta original, el método de deshacer es virtual (bueno, está destinado a ser - el = 0 en la clase base lo delata, pero la palabra clave virtual parece que se dejó accidentalmente) – camh

7

Además de Daniel Earwicker's answer, podemos usar plantillas variadic en el nuevo estándar de C++ para lograr lo mismo.

template <typename T> 
struct Field { 
    typename T::value_type storage; 

    typename T::value_type &operator[](const T &c) { 
    return storage; 
    } 
}; 

template<typename... Fields> 
struct ctmap : public Field<Fields>... { 
}; 

Este código es más limpio y no tiene un límite fijo de miembros. Puede usarlo de la misma manera

struct age { typedef int value_type; }; 
struct last_name { typedef std::string value_type; }; 

ctmap<last_name, age> person; 

person[last_name()] = "Smith"; 
person[age()] = 104; 
+2

Puede ir [un paso más allá] (http://coliru.stacked-crooked.com/a/c5ec6cdf781525a1), donde nos acercamos aún más al uso de campo. – Yakk

Cuestiones relacionadas