the_mandrill
the_mandrill

Reputation: 30862

How to expose fields of a class by name

I want to define structs to hold various application parameters:

struct Params 
{
  String fooName;
  int    barCount;
  bool   widgetFlags;
  // ... many more
};

but I want to be able to enumerate, get and set these fields by name, eg so that I can expose them to automation APIs and for ease in serialisation:

Params p;
cout << p.getField("barCount");
p.setField("fooName", "Roger");
for (String fieldname : p.getFieldNames()) {
   cout << fieldname << "=" << p.getField(fieldName);
}

Is there a good way of defining a binding from a string label to a get/set function? Along the lines of this (very much pseudocode):

Params() {
  addBinding("barCount", setter(&Params::barCount), getter(&Params::barCount));
  ...

I know that other options are to auto-generate the struct from an external metadata file, and another is to store the struct as a table of (key,value) pairs, but I would rather keep the data in a struct.

I do have a Variant type which all fields are convertible to.

Upvotes: 1

Views: 403

Answers (4)

the_mandrill
the_mandrill

Reputation: 30862

I've managed to come up with something that satisfies my particular need. Ari's answer was closest in terms of mapping strings to references to member variables, though it relied on casting from void*. I've got something that's a bit more type-safe:

There's an interface for an individual PropertyAccessor that has a templated class derived from it which binds to a reference to a specific member variable and converts to and from the Variant representation:

class IPropertyAccessor
{
public:
    virtual ~IPropertyAccessor() {}
    virtual Variant     getValueAsVariant() const =0;
    virtual void        setValueAsVariant(const Variant& variant) =0;

};
typedef std::shared_ptr<IPropertyAccessor> IPropertyAccessorPtr;

template <class T>
class PropertyAccessor : public IPropertyAccessor
{
public:
    PropertyAccessor(T& valueRef_) : valueRef(valueRef_) {}
    virtual Variant getValueAsVariant() const {return VariantConverter<T>().toVariant(valueRef); }
    virtual void setValueAsVariant(const Variant& variant) {return VariantConverter<T>().toValue(variant); }

    T& valueRef;
};

    // Helper class to create a propertyaccessor templated on a type
template <class T>
    static IPropertyAccessorPtr createAccessor(T& valueRef_)
{
   return std::make_shared<PropertyAccessor<T>>(valueRef_);
}

The class exposing a collection can now define an ID -> PropertyAccessor and bind its values by reference:

#define REGISTER_PROPERTY(field) accessorMap.insert(AccessorMap::value_type(#field, createAccessor(field)))
class TestPropertyCollection
{
public:
    typedef std::map<PropertyID, IPropertyAccessorPtr> AccessorMap;
    TestPropertyCollection()
    {
        REGISTER_PROPERTY(stringField1);
        // expands to 
        // accessorMap.insert(AccessorMap::value_type("stringField", createAccessor(stringField)));
        REGISTER_PROPERTY(stringField2);
        REGISTER_PROPERTY(intField1);
    }

  bool getPropertyVariant(const PropertyID& propertyID, Variant& retVal)
    {
        auto it = accessorMap.find(propertyID);
        if (it != accessorMap.end()) {
            auto& accessor = it->second;
            retVal = accessor->getValueAsVariant();
            return true;
        }
        return false;
    }

    String      stringField1;
    String      stringField2;
    int         intField1;

    AccessorMap accessorMap
};

Upvotes: 0

Ari
Ari

Reputation: 1092

C++ doesn't have reflection so this isn't something you can do cleanly. Also, by referring to members as strings, you have to try to side-step the strongly typed nature of the language. Using a serialization library like Boost Serializer or Google Protobuf might be more useful.

That said, if we allow some horribleness, one could do something with an XMacro. (Disclaimer: I wouldn't recommend actually doing this). First you put all the information you need into a macro

#define FIELD_PARAMS \
    FIELD_INFO(std::string, Name, "Name") \
    FIELD_INFO(int, Count, "Count")

Or alternatively into a header file

<defs.h>
 FIELD_INFO(std::string, Name, "Name") \
 FIELD_INFO(int, Count, "Count")

Then you'll define FIELD_INFO inside your class to either mean the member declaration, or adding them to a map

struct Params{
    Params() {
#define FIELD_INFO(TYPE,NAME,STRNAME) names_to_members.insert(std::make_pair(STRNAME,&NAME));
        FIELD_PARAMS
#undef FIELD_INFO
    }

    template <typename T>
    T& get(std::string field){
        return *(T*)names_to_members[field];
    }

    std::map<std::string, void*> names_to_members;

#define FIELD_INFO(TYPE,NAME,STRNAME) TYPE NAME;
    FIELD_PARAMS
#undef FIELD_INFO
};

And then you could use it like this

int main (int argc, char** argv){
    Params myParams;
    myParams.get<std::string>("Name") = "Mike";
    myParams.get<int>("Count") = 38;

    std::cout << myParams.get<std::string>("Name"); // or myParams.Name
    std::cout << std::endl;
    std::cout << myParams.get<int>("Count"); // or myParams.Count

    return 0;
}

Unfortunately you still need to tell the compiler what the type is. If you have a good variant class and libraries that play well with it, you may be able to get around this.

Upvotes: 1

Christian Hackl
Christian Hackl

Reputation: 27538

There is no really good way (with "good" being a very subjective aspect anyway), because whatever technique you choose is not part of the C++ language itself, but if your goal is serialisation, have a look at Boost Serialization.

Upvotes: 0

berkus
berkus

Reputation: 1563

I'm using a slightly different storage for this: here. The tags I use are ints for some reason, but you could use std::string keys just as well.

Upvotes: 0

Related Questions