How can I access a struct property by string name?
Asked Answered
J

6

9

I have a struct:

typedef struct Tick {
    double open;
    double high;
    double low;
    double close;
    double ema100;
} Tick;

I would like to access a property given a key:

Tick currentTick = {44.5, 45.1, 44.2, 44.6, 44.255};
std::string key = "ema100";

std::cout << currentTick[key];

Is there a way to do this without using std::map? I imagine the answer is no, but I just want to be certain before modifying everything to use std::map and increase my memory requirements.

Jacky answered 8/4, 2016 at 18:16 Comment(3)
This answer to a different question explains it pretty well: https://mcmap.net/q/1171121/-convert-member-variable-name-to-a-stringGerita
As an aside, please don't use the typedef struct T { } type; form. This is not ancient C. struct T { }; is all you need to do.Ferroelectric
Good to know -- thank you! This is my first time using C++ again in 12 years :)Jacky
K
10

Is there a way to do this without using std::map?

As long as you are willing to live with a series of cascading if-else statements, you can do that.

I would question the design though.

You can get there partially using tag based member variables. Sample working example:

#include <iostream>

struct OpenTag {};
struct HighTag {};
struct LowTag {};
struct CloseTag {};
struct Ema100Tag {};

struct Tick {

   template <typename Tag> struct Member
   {
      double val;

      operator double () const { return val; }

      operator double& () { return val; }
   };

   struct AllMembers : Member<OpenTag>, 
                       Member<HighTag>,
                       Member<LowTag>,
                       Member<CloseTag>,
                       Member<Ema100Tag> {};

   AllMembers data;

   template <typename Tag>
      double operator[](Tag t) const
      {
         return (Member<Tag> const&)(data);
      }

   template <typename Tag>
      double& operator[](Tag t)
      {
         return (Member<Tag>&)(data);
      }

};

int main()
{
   Tick t;
   t[OpenTag()] = 12.345;
   std::cout << t[OpenTag()] << std::endl;
}

Output:

12.345
Kangaroo answered 8/4, 2016 at 18:20 Comment(0)
I
8

The feature you're looking for is called reflection. This is not supported by native C++. You can either check for some 3rd-party libraries to do the reflection for you (but still would require lot of manual effort).

Or the other option is (as you mentioned) using std::map (or rather std::unordered_map as it would perform better) to map the name to id or offset (pointer) of the field within the class and then via switch statement (in former case) or directly using the pointer (in the latter case) modify the field value.

Intelligible answered 8/4, 2016 at 18:25 Comment(1)
Thanks for the keyword. Now I know what to search!Alix
M
3

You can do that using metadata available at compile time. However, we must do it manually:

template<typename Class, typename T>
struct Property {
    constexpr Property(T Class::*aMember, const char* aName) : member{aMember}, name{aName} {}

    using Type = T;

    T Class::*member;
    const char* name;
};

template<typename Class, typename T>
constexpr auto makeProperty(T Class::*member, const char* name) {
    return Property<Class, T>{member, name};
}

Now we have a class that can hold our desired metadata. Here's how to use it:

struct Dog {
    constexpr static auto properties = std::make_tuple(
        makeProperty(&Dog::barkType, "barkType"),
        makeProperty(&Dog::color, "color")
    );

private:
    std::string barkType;
    std::string color;
};

Now we can do iteration on it by recursion:

template<std::size_t iteration, typename T, typename U>
void accessGetByString(T&& object, std::string name, U&& access) {
    // get the property
    constexpr auto property = std::get<iteration>(std::decay_t<T>::properties);

    if (name == property.name) {
        std::forward<U>(access)(std::forward<T>(object).*(property.member));
    }
}

template<std::size_t iteration, typename T, typename U>
std::enable_if_t<(iteration > 0)>
getByStringIteration(T&& object, std::string name, U&& access) {
    accessGetByString<iteration>(std::forward<T>(object), name, std::forward<U>(access));
    // next iteration
    getByStringIteration<iteration - 1>(std::forward<T>(object), name, std::forward<U>(access));
}

template<std::size_t iteration, typename T, typename U>
std::enable_if_t<(iteration == 0)>
getByStringIteration(T&& object, std::string name, U&& access) {
    accessGetByString<iteration>(std::forward<T>(object), name, std::forward<U>(access));
}

template<typename T, typename U>
void getByString(T&& object, std::string name, U&& access) {
    getByStringIteration<std::tuple_size<decltype(std::decay_t<T>::properties)>::value - 1>(
        std::forward<T>(object),
        name,
        std::forward<U>(access)
    );
}

Then finally, you can use this tool like:

struct MyAccess {
    void operator()(int i) { cout << "got int " << i << endl; }
    void operator()(double f) { cout << "got double " << f << endl; }
    void operator()(std::string s) { cout << "got string " << s << endl; }
}

Dog myDog;

getByString(myDog, "color", MyAccess{});

This for sure could by simplified with overloaded lambda. To know more about overloaded lambda, see this blog post

The original code was taken from that answer: C++ JSON Serialization

There's a proposal to make things like this easier. This is covered by P0255r0

Maryellen answered 8/4, 2016 at 18:39 Comment(0)
E
1

Honestly i think the use of an overloaded operator[] and if-else statement is all you really need. Given any class, are their really that many members? The other solutions in my opinion provide too much overhead for such a simple task. I would just do something like this:

template <typename T>
T& operator[](const std::string& key) 
{
    if (key == ...)
        // do something
}
Empirin answered 8/4, 2016 at 20:0 Comment(1)
In my case there will be over 100 members, so for my particular scenario, an if-else or switch block would be tedious.Jacky
K
1

To add to the if-else answers, I solved the repetition by using macros to automate the process somewhat.

Please note I use a "for each" macro from https://www.scs.stanford.edu/~dm/blog/va-opt.html, and you will need to include that code

#include <string>
#include <typeinfo>
#include <stdexcept>
#include <sstream>

#define _MAPABLE_MEMBERS_GET(name) \
    if (key == #name)              \
        return name;
#define _MAPABLE_MEMBERS_SET(name)                 \
    if (key == #name)                              \
        name = convert<decltype(name)>(new_value); \
    return;
#define _MAPABLE_MEMBERS_HAS(name) \
    if (key == #name)              \
        return true;
#define _MAPABLE_MEMBERS_KEYS(name) #name,

#define MAPABLE_MEMBERS(args...)                                       \
    template <typename T>                                              \
    T &get(const std::string &key)                                     \
    {                                                                  \
        FOR_EACH(_MAPABLE_MEMBERS_GET, args)                            \
        throw new std::runtime_error("mapable member does not exist"); \
    }                                                                  \
    template <typename T>                                              \
    void set(const std::string &key, T new_value)                      \
    {                                                                  \
        FOR_EACH(_MAPABLE_MEMBERS_SET, args)                            \
    }                                                                  \
    void set(const std::string &key, const std::string &new_value)     \
    {                                                                  \
        FOR_EACH(_MAPABLE_MEMBERS_SET, args)                            \
    }                                                                  \
    bool has(const std::string &key)                                   \
    {                                                                  \
        FOR_EACH(_MAPABLE_MEMBERS_HAS, args)                            \
        return false;                                                  \
    }

class MapableMembers
{
public:
    template <typename T>
    T &get(const std::string &key)
    {
        throw new std::runtime_error("MapableMembers::get virtual call");
    }

    virtual void set([[maybe_unused]] const std::string &key, [[maybe_unused]] const std::string &new_value)
    {
        throw new std::runtime_error("MapableMembers::set virtual call");
    }

    virtual bool has([[maybe_unused]] const std::string &key)
    {
        throw new std::runtime_error("MapableMembers::has virtual call");
    }

    virtual ~MapableMembers() = default;

};

template <typename T>
typename std::enable_if<!std::is_enum<T>::value, T>::type
convert(const std::string &str)
{
    T val;
    std::istringstream(str) >> val;
    return val;
}

template <typename T>
typename std::enable_if<std::is_enum<T>::value, T>::type
convert(const std::string &str)
{
    typename std::underlying_type<T>::type val;
    std::istringstream(str) >> val;
    return static_cast<T>(val);
}

You can then use it like this:

class A : public MapableMembers
{
    double a;
    int b;
    float c;
    float d;
    
    MAPABLE_MEMBERS(a, b, c, d)
};

enum class ExampleEnum
{
    Something,
    OtherThing,
    Another,
};

class B : public MapableMembers
{
    ExampleEnum e;
    size_t f;
    short g;
    unsigned int h;
    
    MAPABLE_MEMBERS(e, f, g, h)
};

A a;
B b;

auto /* double */ a_a = a.get("a");
a.set("a", 3.14);
a.set("b", "3.14");
a.has("e"); // false

Mapable m = static_cast<Mapable>(a);
m.has("e"); // false

m = static_cast<Mapable>(b);
m.has("e"); // true
Kial answered 25/1 at 22:39 Comment(0)
T
0

My preferred methodology is to simply return a Map from the function with pointers to the members... (it's not automatic, and it doesn't use any preprocessor magic, but it's simple and easy to read)

This may not work in some situations where a pointer to the value is no sufficient, but I prefer it over getters and setters.

Here's an example from a program I wrote

struct LogThemeSettings {
    static constexpr char FOREGROUND[] = "foreground";
    static constexpr char BACKGROUND[] = "background";
    static constexpr char ITALIC[] = "italic";
    static constexpr char BOLD[] = "bold";
    static constexpr char WEIGHT[] = "weight";
    static constexpr char UNDERLINE[] = "underline";
    static constexpr char CAPITALIZATION[] = "capitalization";
    static constexpr char TRUE[] = "true";
    static constexpr char FALSE[] = "false";

    static std::set<QString> VALID_THEME_ATTRIBUTES;

    std::map<QString, QString> NamedEntity1;
    std::map<QString, QString> NamedEntity2;
    std::map<QString, QString> NamedEntity3;
    std::map<QString, QString> NamedEntity4;
    std::map<QString, QString> NamedEntity5;
    std::map<QString, QString> NamedEntity6;
    std::map<QString, QString> NamedEntity7;
    std::map<QString, QString> NamedEntity8;
    std::map<QString, QString> NamedKey;
    std::map<QString, QString> NamedValue;
    std::map<QString, QString> VariableName;
    std::map<QString, QString> VariableValue;
    std::map<QString, QString> QuotedString;
    std::map<QString, QString> LevelError;
    std::map<QString, QString> LevelWarning;
    std::map<QString, QString> LevelLog;
    std::map<QString, QString> Good;
    std::map<QString, QString> Bad;
    std::map<QString, QString> Uri;
    std::map<QString, QString> ClassType;
    std::map<QString, QString> Category;
    std::map<QString, QString> Time;
    std::map<QString, QString> Address;
    std::map<QString, QString> Numeric;
    std::map<QString, QString> Path;
    std::map<QString, QString> Directory;
    std::map<QString, QString> Foreground;
    std::map<QString, QString> Background;
    std::map<QString, QString> Cursor;

    /**
     * We want the vast majority of code to access these struct members directly.
     * In a couple places (like the UI) that is cubmersome though. This map allows the members
     * of the theme to be accessed by name, and changed via pointer.
     */
    std::map<QString, std::map<QString, QString>*> getMap() {
        return std::map<QString, std::map<QString, QString>*> {
            {"NamedEntity1", &NamedEntity1},
            {"NamedEntity2", &NamedEntity2},
            {"NamedEntity3", &NamedEntity3},
            {"NamedEntity4", &NamedEntity4},
            {"NamedEntity5", &NamedEntity5},
            {"NamedEntity6", &NamedEntity6},
            {"NamedEntity7", &NamedEntity7},
            {"NamedEntity8", &NamedEntity8},
            {"NamedKey", &NamedKey},
            {"NamedValue", &NamedValue},
            {"VariableName", &VariableName},
            {"VariableValue", &VariableValue},
            {"QuotedString", &QuotedString},
            {"LevelError", &LevelError},
            {"LevelWarning", &LevelWarning},
            {"LevelLog", &LevelLog},
            {"Good", &Good},
            {"Bad", &Bad},
            {"Uri", &Uri},
            {"ClassType", &ClassType},
            {"Category", &Category},
            {"Time", &Time},
            {"Address", &Address},
            {"Numeric", &Numeric},
            {"Path", &Path},
            {"Directory", &Directory},
            {"Foreground", &Foreground},
            {"Background", &Background},
            {"Cursor", &Cursor},
        };
    }

    // now that we can access the members via iterator it's easy to implement this
    // `isValid()` function to check that all members have values.
    bool isValid() {
        auto members = getMap();
        for(std::map<QString, std::map<QString, QString>*>::iterator it = members.begin(); it != members.end(); ++it) {
            if(it->second->empty()) {
                return false;
            }
        }
        return true;
    }
Toy answered 22/1 at 0:30 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.