3

Let's say I want to define classes of the following structure:

struct MyClass {
    int x;
    bool y;
    float z;
    MyClass(QVariantMap data) : x(data["x"]), y(data["y"]), z(data["z"]) {}
};

As you can see, I have a QVariantMap (something similar to std::map<std::string, boost::variant<...>> for those of you not familiar with Qt) from which I want to be able to construct such a type with no further knowledge of its fields, hence the convenient constructor which should "deserialize" the fields from the map.

I need several classes of this style, and I want the definitions to be as clean as possible for maximum maintainability (regarding typos in the string keys), readability and automation.

I thought of a macro structure like the following:

DEF_CLASS(MyClass)(
    DEF_FIELD(int, x)
    DEF_FIELD(bool, y)
    DEF_FIELD(float, z)
);

I don't see any problem when I only want to generate the fields but not the constructor (the other way around is also possible, but I'll demonstrate the former only):

#define DEF_CLASS(CLASSNAME) \
    struct CLASSNAME { \
    _DEF_CLASS_TAIL/*..."curry the arguments"...*/
#define _DEF_CLASS_TAIL(FIELDS) \
        FIELDS \
    }
#define DEF_FIELD(TYPE, NAME) TYPE NAME;

I define the first macro which starts the class definition and use a "curry" technique to forward the second parentheses to a second macro which puts the contents of the class definition (FIELDS) and closes it afterwards. This way -- so was my idea -- I have access to the FIELDS within the second macro.

But how can I now output the fields twice, one to define the actual fields and one to output the member initialization?

I know that if I define the macro DEF_CLASS_FIELD in two different ways and then somewhat "include" the fields from my definition code above, one for every macro definition, I could print them one after another correctly. But since the field listing is (and should) be within the class definition, I cannot simply include something twice.

Are there other options?

I try to avoid the Boost preprocessor library, but if you have a nice solution which uses that, go ahead. However, I very much prefer a simple solution, if there is any.

3
  • It's not the solution that you want, but the simplest thing is of course to just keep a reference/pointer to the QVariantMap in the struct. Since all your structs would have been different (or you wouldn't need the macros), you presumably have to write code somewhere that would be specialized for each specific struct. In that code, you could cache the values from the QVariantMap held by the struct, or just use the map lookup each time. Commented May 8, 2013 at 20:02
  • Yeah, actually my current temporary solution is exactly that... I store the variant map in the class and let the macro generate a getter for each "field" with the access (and conversion) for each call. My primary problem with this approach is not the performance but the "late check" if the map really contains the expected fields. In the constructor I'd like to call some error handler (or throw an exception) if the data is in the wrong format... Commented May 8, 2013 at 20:06
  • Well if you're ok doing the map lookup every time, then there's no need to cache the values. In that case, instead of DEF_FIELD, maybe you could just make a macro that takes in a map key (you can use #x to generate "x") and a type, and then performs the check: if (typeid(map[KEY])!=typeid(TYPE)) { throw; }? Commented May 8, 2013 at 23:17

1 Answer 1

1

Here is an example which does not actually build a struct to cache the values from the map, but simply checks at the constructor that the map contains the fields, as discussed in the comments of the opening question.

#define DEF_CLASS(CLASSNAME) \
    struct CLASSNAME { \
    CLASSNAME(QVariantMap& map) {\
    _DEF_CLASS_TAIL/*..."curry the arguments"...*/
#define _DEF_CLASS_TAIL(FIELDS) \
        FIELDS \
    }};
#define CHK_FIELD(TYPE, NAME) \
    if (typeid(TYPE)!=typeid(map[#NAME])) \
    { throw std::runtime_error(#NAME" missing or wrong type");}
DEF_CLASS(MyClass)(
    CHK_FIELD(int, x)
    CHK_FIELD(bool, y)
    CHK_FIELD(float, z)
);

This produces the following from the preprocessor (after running through astyle):

struct MyClass {
   MyClass(QVariantMap& map) {
      if (typeid(int) != typeid(map["x"])) {
         throw std::runtime_error("x"" missing or wrong type");
      }
      if (typeid(bool) != typeid(map["y"])) {
         throw std::runtime_error("y"" missing or wrong type");
      }
      if (typeid(float) != typeid(map["z"])) {
         throw std::runtime_error("z"" missing or wrong type");
      }
   }
};

EDIT: I'm not 100% sure the typeid comparisons will work like this, but it should be straight forward to replace it with a check that does work.

Sign up to request clarification or add additional context in comments.

3 Comments

Thank you very much. QVariant uses .canConvert<T>() and then .value<T>(), but as you said: It's straightforward to adapt this. However: if I don't provide the data as fields, I want to have them as getters, and your solution doesn't have any of both (since you "spent" the macro expansion on the checks). Maybe I can combine both class definitions (your "checker" and my current workaround with getters but no checks) by including the definitions twice, first generating the checkers, then the actual classes which use the checkers in their constructor.
(cont.) The problem discussed in the question with the multiple includes with multiple definitions mainly applied to the case where I had to put two different things within a class (like fields and constructor implementation), which I expect to be pretty hard, if not impossible.
That could work.. or you could change DEF_CLASS so that it has struct CLASSNAME { QVariantMap& fMap; CLASSNAME(QVariantMap& map) : fMap(map) {\. Now the ctor that does the checking also stores a reference to the map, so later in the code when you need the getters, you just do objOfMyClass.fMap["x"] -- since I'm still assuming that when you want to use the values from the map, you have to get specific and type it out anyway.

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.