Search code examples
c++attributes

How to programmatically assign class/struct attributes?


I am attempting to translate my Python program to C++, but because I am new to C++ I am encountering some problems. The input file is first parsed (works, not shown) to create the INITIAL_VALUES dict/map, which I then want to use to assign the Parameters class/struct attributes using the DEST_DICT_PARAMS dict/map.

I was able to achieve this in Python code with:

import dataclasses

INITIAL_VALUES = {
    "BULK": {
        "MAGMA": {
            "M0":    1.0,
            "T0": 1320.0,
        },
        "ASSIM": {
            "M0":    0.0,
            "T0":  600.0,
        },
    }
}
DEST_DICT_PARAMS = {
    'M0': {"MAGMA": 'Mm0', "ASSIM": 'Ma0'},
    'T0': {"MAGMA": 'Tm0', "ASSIM": 'Ta0'},
}

@dataclasses.dataclass
class Parameters:
    Mm0: float = None
    Ma0: float = None
    Ta0: float = None
    Tm0: float = None


class ParametersReader:
    def __init__(self):
        self.parameters = Parameters()
        self._assignParameters()

    def _assignParameters(self):
        for param_fam, dest in DEST_DICT_PARAMS.items():
            for component, param in dest.items():
                value = INITIAL_VALUES["BULK"][component][param_fam]
                setattr(self.parameters, param, value)

params = ParametersReader()
print(params.parameters)

Output:

Parameters(Mm0=1.0, Ma0=0.0, Ta0=600.0, Tm0=1320.0)

So I wrote the corresponding C++ code:

#include <iostream>
#include <map>

using std::map;
using std::string;

map<string, map<string, map<string, float> > > INITIAL_VALUES = {{
    "BULK", {
        {"MAGMA", {
            {"M0",    1.0},
            {"T0", 1320.0},
        }},
        {"ASSIM", {
            {"M0",    0.0},
            {"T0",  600.0},
        }},
    }
}};

map<string, map<string, string> > DEST_DICT_PARAMS = {{
    {"M0", {{"MAGMA", "Mm0"}, {"ASSIM", "Ma0"}}},
    {"T0", {{"MAGMA", "Tm0"}, {"ASSIM", "Ta0"}}},
}};

struct Parameters {
    float Mm0;
    float Ma0;
    float Ta0;
    float Tm0;
} parameters;

class ParametersReader {
public:
    void assignParameters_() {
        map<string, map<string, string> >::iterator itr0;
        map<string, string>::iterator itr1;

        for (itr0 = DEST_DICT_PARAMS.begin(); itr0 != DEST_DICT_PARAMS.end(); itr0++) {
            for (itr1 = itr0->second.begin(); itr1 != itr0->second.end(); itr1++) {
                parameters.itr1->second = INITIAL_VALUES["BULK"][itr1->first];
            }
        }
    }
};

int main() {
    ParametersReader params;
    params.assignParameters_();
}

But I'm getting an error at the line parameters.itr1->second = INITIAL_VALUES['BULK'][itr1->first] saying "no member named 'itr1' in 'Parameters'". That error makes total sense because the code is literally trying to interpret 'itr1' as an attribute name and not the whole 'itr1->second' as the name. I think this comes down to the fact that I can't seem to find a C++ equivalent to Python's setattr(obj, name, val) function that takes an object and its attribute name and assigns it a value. Is there a C++ solution to what I am attempting?

Perhaps my entire approach is incompatible with C++. If so, would you kindly suggest an alternative approach? I would like to keep the input file format the same between the Python and C++ versions.


Solution

  • C++ does not have runtime reflection like Python. You cannot look up a class member by name using a runtime string because class member names do not exist at runtime.

    What you can do is look up a class member via a pointer to member. This is an offset into the object calculated at compile time by the compiler:

    std::map<std::string, std::map<std::string, float Parameters::*> > DEST_DICT_PARAMS = {{
        {"M0", {{"MAGMA", &Parameters::Mm0}, {"ASSIM", &Parameters::Ma0}}},
        {"T0", {{"MAGMA", &Parameters::Tm0}, {"ASSIM", &Parameters::Ta0}}},
    }};
    
    class ParametersReader {
    public:
        void assignParameters_() {
            for (auto& [param_fam, dest] : DEST_DICT_PARAMS) {
                for (auto& [component, param] : dest) {
                    parameters.*param = INITIAL_VALUES["BULK"][component][param_fam];
                }
            }
        }
    };
    

    Demo

    Note I've also used range-based for loops and structured bindings to clean up your assignParameters_ function.