tags:

views:

711

answers:

8

Let's suppose I have a struct like this:

struct my_struct
{
  int a;
  int b; 
}

I have a function which should set a new value for either "a" or "b". This function also requires to specify which variable to set. A typical example would be like this:

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

For reasons I won't write here I cannot pass the pointer to a/b to f. So I cannot call f with address of my_struct::a or my_struct::b. Another thing I cannot do is to declare a vector (int vars[2]) within my_struct and pass an integer as index to f. Basically in f I need to access the variables by name.

Problem with previous example is that in the future I plan to add more variables to struct and in that case I shall remember to add more if statements to f, which is bad for portability. A thing I could do is write f as a macro, like this:

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

and then I could call FUNC(a) or FUNC(b).

This would work but I don't like using macros. So my question is: Is there a way to achieve the same goal using templates instead of macros?

EDIT: I'll try to explain why I cannot use pointers and I need access to variable by name. Basically the structure contains the state of a system. This systems needs to "undo" its state when requested. Undo is handled using an interface called undo_token like this:

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

So I cannot pass pointers to the undo method because of polimorfism (mystruct contains variabiles of other types as well).

When I add a new variable to the structure I generally also add a new class, like this:

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}
};

Problem is I don't know pointer to s when I create the token, so I cannot save a pointer to s::a in the costructor (which would have solved the problem). The class for "b" is the same, just I have to write "s->b" instead of s->a

Maybe this is a design problem: I need an undo token per variable type, not one per variable...

+2  A: 

You can't use templates to solve this, but why use a struct in te first place? This seems like an ideal use for a std::map which would map names to values.

anon
because "f" is just one function needed to do secondary stuff on the structure. I don't want the structure shape to be bound to what "f" needs.
happy_emi
+4  A: 

I'm not sure why you cannot use a pointer so I don't know if this is appropriate, but have a look at http://stackoverflow.com/questions/670734/c-pointer-to-class-data-member, which describes a way you can pass a pointer to a data member of a struct/class that does not point directly to the member, but is later bound to a struct/class pointer. (emphasis added after the poster's edit explaining why a pointer cannot be used)

This way you do not pass a pointer to the member - instead it is more like an offset within a object.

camh
+9  A: 
#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;
}
Mykola Golubyev
Did you read the bit where he said he couldn't take the address of a or b?
anon
Mykola Golubyev
Perfect! This solved my problem. Clever AND simple. Thanks
happy_emi
Couldn't resist adding a template parameter so that the undo_member can be applied to the string field as well as the int. Feel free to roll back if unsolicited edits annoy you! :)
Daniel Earwicker
+1. You can in fact avoid storing member_ at all, since pointers to members can be used as non-type template parameters -- see my answer.
j_random_hacker
@Earwicker: Of course this code can be generalized to all cases where undo can be made with just one assignment.
happy_emi
+2  A: 

It sounds like what you're looking for is called "reflection", and yes it's often implemented with some combination of templates and macros. Be warned that reflection solutions are often messy and annoying to work with, so you may want to do some research into them before you dive into the code to find out if this is really what you want.

Second hit on Google for "C++ reflection templates" was a paper on "Reflection support by means of template metaprogramming". That should get you started. Even if it's not quite what you're looking for, it may show you a way to solve your problem.

Dan Olson
+1  A: 

From what you described, i am guessing you have no way of redefining the structure.

If you did, i'd suggest you use Boost.Fusion to describe your structure with template-named fields. See associative tuples for more information on that. Both kinds of structures might actually be compatible (same organization in memory), but i'm pretty sure there is no way to get such a guarantee from the standard.

If you don't, you can create a complement to the structure that would give you access to fields the same way that associative tuples do. But that can be a bit verbal.

EDIT

Now it's pretty clear that you can define the structures the way you want to. So i definitely suggest you use boost.fusion.

Benoît
+12  A: 

To answer the exact question, there is, but it's pretty complicated, and it will purely be a compile-time thing. (If you need runtime lookup, use a pointer-to-member - and based on your updated question, you may have misunderstood how they work.)

First, you need something you can use to represent the "name of a member" at compile time. In compile-time metaprogramming, everything apart from integers has to be represented by types. So you'll use a type to represent a member.

For example, a member of type integer that stores a person's age, and another for storing their last name:

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

Then you need something like a map that does lookup at compile time. Let's called it ctmap. Let's give it support for up to 8 members. Firstly we need a placeholder to represent the absence of a field:

struct none { struct value_type {}; };

Then we can forward-declare the shape of 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;

We then specialise this for the case where there are no fields:

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

The reason for this will be come clear (possibly) in a moment. Finally, the definition for all other cases:

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; }
};

What the hell's going on here? If you put:

ctmap<last_name, age> person;

C++ will build a type for person by recursively expanding the templates, because ctmap inherits from itself, and we provide storage for the first field and then discard it when we inherit. This all comes to a sudden stop when there are no more fields, because the specialization for all-none kicks in.

So we can say:

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

It's like looking up in a map, but at compile time, using a field-naming class as the key.

This means we can also do this:

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

That's a function that prints one member's value, where the member to be printed is a type parameter. So we can call it like this:

print_member<age>(person);

So yes, you can write a thing that is a little like a struct, a little like a compile-time map.

Daniel Earwicker
Not exactly the cleanest thing in the world, but clever nonetheless. +1 just for teaching me something new.
Dan Olson
A very complete answer, but it's a kind of overkill for what I need. As Dan said, you got a +1 for teaching me something new, anyway.
happy_emi
Thanks. I hope someone stumbles on it and finds it useful - the title of the question seems likely to work that way!
Daniel Earwicker
+1 for reading and actually understanding Abrahams/Gurtovoy "C++ Template Metaprogramming".
Functastic
+1, very cunning! (Is this how Boost.Fusion works?)
j_random_hacker
Abrahams is the dude - I once submitted a minor enhancement to boost-parameter and he explained to me that it need fixing so it ran in O(n) at compile time. It was a little odd when you're just getting use to the idea of template metaprogramming to find that the experts do it in their sleep.
Daniel Earwicker
+1  A: 

I can't think of a reason why you would not have everything at hand when creating an undo command. What you want to be able to undo, you have done. So i believe you can use pointers to class members and even pointers to the fields of a particular class instance when creating the undo command.

You're right in your EDIT section. It is a matter of design.

Benoît
Because there are some points in the code where system state is defined by "checkpoints". This is implmented deleting current state and creating a new one. It's true that in 90% cases pointer is the same, but this is not always the case.
happy_emi
If you delete a state object and create a new one, why don't you find a way not to delete previous state, but to disable it so that, if you undo the deletion, you're once again working with the very same object ? I might not be clear, sorry, but i'm not sure i understood well !
Benoît
Well I suppose that if I can assert that system state pointer is always valid than there is no point in my question.Still, I'm not sure that modifing existing code so that it can fit to "support" one is a good idea. Also see my comment to Neil Butterworth answer.
happy_emi
+5  A: 

Mykola Golubyev's answer is good, but it can be improved slightly by using the fact that pointers to members can be used as non-type template parameters:

#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;
}

This shaves off the cost of a pointer to member from each instance of undo_member.

j_random_hacker
+1. Never saw this one. But this will create a lot of classes one for each struct member.
Mykola Golubyev
@Mykola: That's true, but is it a problem? The methods will all be inlined by the compiler, so I don't think there will be any code bloat.
j_random_hacker
They wont be inlined, since in the original question, the undo method is virtual (well, meant to be - the = 0 in the base class gives that away, but the virtual keyword looks like it was accidently left off)
camh
@camh: If the compiler can determine the dynamic type of an expression, it will call methods directly, without using virtual dispatch (and possibly even inline them). This is the case here as the um1 and um2 variables are full undo_member<X, Y> types.
j_random_hacker
An example of when the compiler *can't* determine the dynamic type would be if um1 or um2 was declared as a pointer/reference to the base class undo_token. In this case virtual dispatch *would* be used.
j_random_hacker