map strings to class members

Viewed 827

Say I have a class / struct like below:

struct A {
    uint32_t a;
    uint8_t  b;
    uint16_t c;
};

And I have a set of strings that associate to each member of A (could be of different integer types, but not non-integer types such as strings), e.g.

"field1" -> A::a
"field2" -> A::b
"field3" -> A::c

Assume that there is always a 1:1 mapping between the strings and members. Is there an elegant way to map each string to each member using something like std::unordered_map? I want to be able to read and write to each field using the strings as keys, e.g.

A a {1,2,3};
mymap["field1"] = 4; // a.a = 4
mymap["field2"] = 5; // a.b = 5
auto c = mymap["field3"]; // c = a.c = 3

I'm using C++11/14 and can't use boost.


Some more information on the context of this question: The struct A mentioned in the question are settings of the program. They are hardware configuration parameters and my software program is used to simulate hardware behaviours. These settings/configurations are script-generated structs like above. We read / write these struct members and because the amount of these settings/configurations are so many (a few thousands), it would be convenient to be able to access them by their names as well. This is how & why I want to associate each member with a string. Whether it is subscripting or a function to access the corresponding members does not really matter, but there is such a 1:1 mapping between a string (name of the setting) and the generated struct member. And as I mentioned in the question, these members are of different integer types.

5 Answers
template<class V>
struct pseudo_ref_t {
  operator V()&& { return getter(); }
  void operator=(V v)&&{
    setter(std::move(v));
  }

  std::function<void(V)> setter;
  std::function<V()> getter;
};
template<class T, class V>
struct member_t {
  friend pseudo_ref_t<V> operator->*( T* t, member_t const& self ) {
    return {
      [&self, t](V in){ self.setter(*t, std::move(in)); },
      [&self, t]()->V{ return self.getter(*t); }
    };
  }
  friend V operator->*( T const* t, member_t const& self ) {
    return self.getter(*t);
  }
  std::function<void(T&, V)> setter;
  std::function<V(T const&)> getter;
};
template<class T, class V, class X>
member_t<T, V> make_member( X T::* mem_ptr ) {
  return {
    [mem_ptr](T& t, V in) {
      (t.*mem_ptr) = std::move(in);
    },
    [mem_ptr](T const& t)->V {
      return (t.*mem_ptr);
    }
  };
}

a member_t<A, uint32_t> can type-erase any member of A that implicitly convertible to/from a uint32_t.

It acts like a smart member pointer.

You could make a proxy-class that wraps around a integer, then store this proxy class in a std::unordered_map.

#include <iostream>
#include <functional>
#include <unordered_map>

struct A {
    uint32_t a;
    uint8_t  b;
    uint16_t c;
};

struct ValueWrapper {
    using value_type = uint64_t;

    template <typename Obj, typename T>
    ValueWrapper(Obj& obj, T Obj::*member) {
        get = [&, member]() { return obj.*member; };
        set = [&, member](value_type value) mutable { obj.*member = value; };
    }

    ValueWrapper()  = default;

    ValueWrapper& operator=(value_type value) {
        set(value);
        return *this;
    }

    operator value_type() {
        return get();
    }

    std::function<value_type()> get;
    std::function<void(value_type)> set;
};

std::unordered_map<std::string, ValueWrapper> make_map(A& a) {
    std::unordered_map<std::string, ValueWrapper> map;

    map["field1"] = ValueWrapper(a, &A::a);
    map["field2"] = ValueWrapper(a, &A::b);
    map["field3"] = ValueWrapper(a, &A::c);

    return map;
}

int main() {
    A a{1,2,3};

    auto map = make_map(a);

    map["field2"] = 67;

    std::cout << a.a << " " << static_cast<int>(a.b) << " " << a.c << std::endl;
    std::cout <<  map["field1"] << " " <<  map["field2"] << " " <<  map["field3"] << std::endl;
}

You do get some restrictions depending on the value_type. If you use int64_t you could wrap anything but a uint64_t safely. If you go for a uint64_t you could wrap all the unsigned integers, but not the signed ones safely.

I put the default constructor there to satisfy unordered_maps use of operator[].

you said that it could have different integer types, but it is a problem for having an elegant solution. If you can set up on a single type, this would be simple, like the following.

 #include <iostream>
 #include <map>

 struct A {
     int a;
     int b;
     int c;
 };
 using namespace std;

 int main() {
     map<string, int A::*> abc = {
         {"a", &A::a},
         {"b", &A::b},
         {"c", &A::c}
     };

     A aa;

     aa.*abc["a"] = 1;
     aa.*abc["b"] = 2;
     aa.*abc["c"] = 3;

     cout << "a = " << aa.a << "(" << aa.*abc["a"] << ")" << endl;
     cout << "b = " << aa.b << "(" << aa.*abc["b"] << ")" << endl;
     cout << "c = " << aa.c << "(" << aa.*abc["c"] << ")" << endl;

     return 0;
 }

The main challenge of your question is that different members have different types. If you make the types the same, you can use a lot of tricks.

For your case, I know you don't want to use Boost or C++17, but just to show you the challenges ahead, let me give you a Boost.Hana, C++17 solution.

#define  BOOST_HANA_CONFIG_ENABLE_STRING_UDL
#include <boost/hana/equal.hpp>
#include <boost/hana/string.hpp>
#include <cstdint> // uint
#include <cassert>

struct A {
    uint32_t a;
    uint8_t  b;
    uint16_t c;
};

struct map{
    A& aa_;
    template<class String>
    decltype(auto) operator[](String s) const{
        using namespace boost::hana::literals;
        if constexpr(s == "field1"_s) return (decltype(aa_.a)&)(aa_.a);
        if constexpr(s == "field2"_s) return (decltype(aa_.b)&)(aa_.b);
        if constexpr(s == "field3"_s) return (decltype(aa_.c)&)(aa_.c);
    }
};

using namespace boost::hana::literals;

void f(uint32_t& a){ a = 3.;}

int main(){
    A aa{1,2,3};
    map mymap{aa};
    mymap["field1"_s] = 4; assert(aa.a == 4);
    mymap["field2"_s] = 5; assert(aa.b == 5);
    mymap["field3"_s] = 6; assert(aa.c == 6);
    auto c = mymap["field3"_s]; assert( c == aa.c );
    mymap["blabla"_s]; // is void (not a compile error)
    assert( map{aa}["field1"_s] == 4 );
}

From this you can walk backwards and perhaps figure out a C++14, the challenge ahead is that you have to implement your own compile time string literals and equality. In other words, reimplement your own Hana string strings: https://www.boost.org/doc/libs/1_61_0/libs/hana/doc/html/namespaceboost_1_1hana_1_1literals.html

The simple solution is that if you have a script that can generate the struct then you can have your script generate your setters.

Start with a simple function interface.

struct A {
    uint32_t a;
    uint8_t  b;
    uint16_t c;
};

typedef std::function<void(A &,string)> SetterType

Then create a lookup table

std::map<std::string,SetterType> Lookup;

and for each field, using your script, generate a parser and setter;

void A_a(A & data, std::string input){
        data.a = std::stoi(input);
}

and then

Lookup["a"] = &A_a;

and use it like

Lookup["a"]("10");

If you can't modify the generating script then maybe you can use a third party parser such as swig or clang to read your structures and generate you a parse tree that you can then use to generate your lookup tables.

Or just use a system that already maps strings to C++. A C++ JSON generator.

https://nlohmann.github.io/json/

Related