5

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.

  • Have you seen Protocol Buffers? – Robert Harvey Jun 13 '18 at 19:57
  • 2
    @RobertHarvey If i had a dollar for every time... ok, fine. Hit me, how would Protocol Buffers help in this situation? I thought they were for binary communication. – opa Jun 13 '18 at 20:01
  • 3
    Reconsider the library option. You're going to significantly reduce the amount of heavy lifting if you can use an already-existing library. This one seems exceptionally well-suited to your use case. It's a single header file with about 18000 lines of template code, and it's MIT-licensed. At the very least, you could glean some good ideas from it. – Robert Harvey Jun 13 '18 at 21:39
  • 3
    For what it's worth, one fairly widely used class library did once use macros very similar to this, for similar reasons. That library was MFC--perhaps the single most despised class library in history though. I doubt those particular macros were the sole (or probably even primary) reason for its being despised, but it's probably not exactly the most promising example to follow either. – Jerry Coffin Jun 14 '18 at 0:20
  • 1
    @RobertHarvey Your link is invalid – opa Jun 14 '18 at 13:50
8

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"

  • 4
    I never considered that maybe reducing all boiler plate is not necessarily congruent with maintainable code. – opa Jun 14 '18 at 14:04
11

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.)

  • So I would do templates if I could. I was using QT for the rest of the project, and I'm only really only using QT for properties for these classes, however this requires macros to be defined for the class. QTs macros provide ways for me to query the macros on objects with out knowing the types and having to know the child class at compile time, this would need to be a requirement for any replacement. Additionally I need some way to construct the child classes from string of the class name from gui (hence the use of a map). It looks like the only way to do this per class it through... – opa Jun 14 '18 at 15:11
  • ... that static class instantiation method, I couldn't figure out how to do this at compile time and still add something to the map per class. – opa Jun 14 '18 at 15:11
7

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.

1
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.

  • very good answers all around, I didn't even consider this. – opa Jun 18 '18 at 16:55

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.