Question

I've got a set of classes that all inherit from a base class that are responsible for different functions (sort of like a group of "operators") They all work on the same input and output the same output type, just different operations are performed internally with some other state. They need to be serialized to JSON and factoried from a UI.

My requirements are that people need to be able to work on this without struggling to understand it. I was hoping some one else had employed this in a different project with other people and could tell me whether or not this is a bad idea from experience and why.

In order to do this I've employed maps that associate the class name to the constructor and method to serialize the class. At first I was manually entering class information in a separate file that housed these maps, then I found a method to do so utilizing static class objects, where they automatically add themselves to the parent abstract classes static map. Because this was needed for each class no matter what, and had the same exact format except for the name of the class used, I created two macros to help with this (so you would only need do MACRO_TAG_CLASS(classname) inside class declaration and MACRO_REGISTER_CLASS(classname) after declaration).

Now I've come to refactor the way I was doing serialization and GUI display, allowing less work for people creating new classes of this type and making them more versatile for how I now want to use them. I ended up needing to use QT properties in the same way with every class, so I decided to make a macro to deal with this as well (Note I'm using boost pre-processor to help out)

Now I have classes that all look like this:

class MyClass :
        public AbstractBase {
Q_OBJECT
private:
    TypeA m_foo;
    TypeB m_bar;
public:
    MACRO_TAG_CLASS(MyClass)

MACRO_ADD_PROPERTIES((TypeA, foo), (TypeB, bar))
//adds qt properties, signals, and adds the strings of the properties to a list for the class

   //other class functions...
   //constructor that is different per class

   //function every class in this heirarchy has, sort of operator(), 
   //but class is doesn't correspond 1:1 with the concept of an operator, so not a functor.
    AbstractBase *baz(X *x) override;

    virtual ~MyClass() = default;
};
MACRO_REGISTER_CLASS(MyClass)

and I was thinking of changing them to something that looks like this:

#define START_CLASS_OF_BASE(CLASS_NAME, ...)\
class CLASS_NAME : public AbstractBase { \
Q_OBJECT \
 public: \
    MACRO_TAG_CLASS(CLASS_NAME) \
MACRO_ADD_PROPERTIES(__VA_ARGS__) \
private:

#define END_CLASS_OF_BASE(CLASS_NAME) \
}; \
    MACRO_REGISTER_CLASS(CLASS_NAME)

START_CLASS_OF_BASE(MyClass, (TypeA, foo), (TypeB, bar))
private:
    TypeA m_foo;
    TypeB m_bar;
public:
   //other class functions...
   //constructor that is different per class

    AbstractBase *baz(X *x) override;

    virtual ~MyClass() = default;
END_CLASS_OF_BASE(MyClass)

Note this pattern only appears in this particular class hierarchy, other people will need to maintain this code eventually, and there are about 20 classes that fit this pattern.

Was it helpful?

Solution

I oppose to use macros instead of normal class here because:

  1. It is harder to debug, especially when it has compile error

  2. If the cost of creating a new class easily is harder to maintain later, I prefer do more works to create a class but easier to maintain later

  3. I believe a gold of quote would be applied in this case :"Reading code usually cost more time than writing code"

OTHER TIPS

I currently work on a code base that has classes created with these types of macros. I would strongly discourage doing things this way because if anything goes wrong in the future with any of the macro-ized code it's impossible to debug. It's extremely difficult to change the class in any way. And the macros will likely get manually expanded in the future as you find one-offs and edge cases where one particular class created this way needs "just one thing different". Then future changes to the macro don't affect that one class, unless you remember to also update that expanded copy. If there's any way you can use C++ templates for this instead, that would be preferable. (Or as suggested by commenters use a library that knows how to do this.)

Using macros for this will become a maintenance horror, as others have already pointed out. As an alternative approach, you could implement a small code generator for creating the repeated boilerplate code.

Of course, a code generator has to maintained as well as the macro, but the main advantage is, it produces (hopefully) readable code which will be compiled on its own, can be debugged on its own (without the interfering macro code), and extended individually, if required.

For such code generators, two approaches are possible: either a simple one-time initial generation, and then manual maintenance of the generated code afterwards, or support for regeneration (so you have to care for separation of generated and manually written code). Both approaches may be suitable for your case, you have to decide what fits best.

This typically pays off when the number of classes will grow in the future, the break-even point can be somewhere between 5 and 50 classes, depending on how complex the requirements are and how often you can use the generator.

class MyClass :
        public AbstractBase {
Q_OBJECT

You're not writing C++ code here, you're writing Qt code. Critically, this is code that is first processed by the Qt moc compiler. It expects to see Q_OBJECT macro in a class, and will generate the metadata for that class.

So let's look at your macro approach:

class CLASS_NAME : public AbstractBase { \
Q_OBJECT \

Even if moc would spot the Q_OBJECT, it would generate metadata for CLASS_NAME. That's of course not the class name, that's a macro parameter. The problem is that moc can't figure out all macro instantiations, and therefore can't generate the metadata.

The fundamental problem here is that you're working with a staged model of compilation, with both a Qt preprocessor and a C preprocessor. One of the realizations of C++ was that the C preprocessor was inconvenient from a software engineering perspective. That's why C++ templates are integral to the language.

That doesn't help you with Qt or your own preprocessor hacks, though. You have to live with the fact that moc runs first.

Licensed under: CC-BY-SA with attribution
scroll top