Mapping data members of a class

271 Views Asked by At

I am trying to design a data stuctures, which would enhance/supplement an existing one by storing some additional data about it's members.

Let's say we have:

class A {
 int x;
 string y;
};

And we want to have a GUI component associated with it, so the data members have corresponding GUI elements. I'd like to map the members to their respective components. Something like

class GuiA {
 int x;
 string y;
 map<MemberHandle, GuiElement*> guiHandles;
}

I don't have any restrictions, but I'd like the result to be easily convertible to the original type.

I am aware, that I could introduce a template e.g. GuiElementMember holding original data plus the GuiElement pointer, and swap class member for their decorated counterparts, so it would look like:

class GuiA {
 GuiElementMember<int> x;
 GuiElementMember<string> y;
}

but I'd like to avoid it, as it completely changes access patterns to data members and bloats it. I.e. it results with data members interleaved with pointers, that are not easy to strip out.

Ideally it would be possible to write GuiA as a derived class of A, or as a composition of A and something additional.

I was thinking about something like a template that class could produce the map. I could yield to write a custom class per component, but I don't think there is an easy way to map data members, so on the clients side it would look like getGuiMember(GuiA::x). The pointer to data member contains the member original type. I don't think it is possible to have something like "type-erased pointer to member" that could serve as a MemberHandle type.

The only thing that comes to my mind is a custom enum per component which would enumerate data members and serve as key type for a map (or a vector in this case), but it seems as an awful lot of information duplication and maintenance.

Is there some technique that allows mapping data members?

I don't really care about the implementational complexity as long as the interface is easy. I welcome boost or template magic. I also don't care about the performance of additional data access, it's extra stuff, but the plain class usage should not be impacted, so introduction of indirection that cannot be optimized is less welcomed.

EDIT: Please don't hinge on GUI thing it's an example. I am only concerned about storing some additional data per member without composing it with the member.

3

There are 3 best solutions below

0
srdjan.veljkovic On

In general, you use macros for such purposes. They can generate any kind of code/wrappers that you'd like, letting you have the usual access to your data, but also adding stuff you want/need. It ain't pretty, but it works.

There are some template libraries that can help here, like Boost.Fusion or Boost.Hana, but, you can also roll your own here if you don't have a use for their advanced features (which come with the long compilation price tag).

Also, if you can focus on a particular GUI framework, they have some support for such things. For example, Qt has its own "meta object" compiler.

1
Hitobat On

You could try a template for this? e.g.

template <typename T>
class GuiItem : public T {
    map<MemberHandle, GuiElement*> guiHandles;
}

GuiItem<A> guiA;
guiA.x = 123;
guiA.y = "y";
guiA.guiHandles[handle] = element;

I'm not sure I understand the other requirements so this way may not work for you.

0
Maxim Egorushkin On

You can use BOOST_FUSION_DEFINE_STRUCT to define your structures that can be iterated over with a for_each loop:

#include <boost/fusion/include/define_struct.hpp>
#include <boost/fusion/include/for_each.hpp>
#include <unordered_map>
#include <string>
#include <cstdint>

BOOST_FUSION_DEFINE_STRUCT(
    (demo), employee,
    (std::string, name)
    (int, age)
    )

struct GuiElement;
GuiElement* createGuiElement(char const* name);

using Mapping = std::unordered_map<size_t, GuiElement*>;

template<class T>
Mapping create_mapping(T&& t) {
    Mapping mapping;
    boost::fusion::for_each(t, [&](auto& member) {
        auto offset = reinterpret_cast<uintptr_t>(&member) - reinterpret_cast<uintptr_t>(&t);
        mapping[offset];
    });
    return mapping;
}

template<class T, class M>
GuiElement*& get_mapping_element(Mapping& mapping, T const& t, M const& member) {
    auto offset = reinterpret_cast<uintptr_t>(&member) - reinterpret_cast<uintptr_t>(&t);
    auto found = mapping.find(offset);
    if(found == mapping.end())
        std::abort();
    return found->second;
}

int main() {
    auto employee_mapping = create_mapping(demo::employee{});

    demo::employee e1;
    get_mapping_element(employee_mapping, e1, e1.name) = createGuiElement("name");
    get_mapping_element(employee_mapping, e1, e1.age) = createGuiElement("age");
}

In the code there is a Mapping, one per class. Each member is identified by its offset from the beginning of its enclosing class.