简体   繁体   中英

Can templates be used to access struct variables by name?

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 polymorphism (mystruct contains variables 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 constructor (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...

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 .

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

In addition to Daniel Earwicker's answer , we can use variadic templates in the new C++ standard to achieve the same.

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

This code is cleaner and does not have fixed bound of members. You can use it in the same way

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;

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 .

I'm not sure why you cannot use a pointer so I don't know if this is appropriate, but have a look at 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.

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.

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.

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.

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.

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM