2

I'm trying to engineer this:

  • 200 subclasses [ Derived Classes ]
  • After a subclass is defined, I wont need to edit any other file. [ Decoupled ]
  • Subclass Definition registers itself. [ Definition is Registration ]

What I could do instead:

Create a class [ One big one ]

Create an enum value (or similar) for every functional variant [ Highly Coupled ]

alter behaviour in a switch [ Cumbersome ]

While that would work with its own advantages, I'd like to see if something more polymorphic, decoupled, and Modular is achievable, like this:

class Base { ... };
static QList<Base> s_RegisteredClasses; // Empty
class A : public Base{ ... };
class B : public Base{ ... };
/* s_RegisteredClasses should now have A & B inside of it */

What I've tried involves static constructors, where every base class has a different signature. The initialization works, but I wouldnt know how to register it:

class Register { public: Register() { /* Static Constructor to run any code */ } };

template<class T>
class Base { static inline Register selfRegister{} }; // `Register` initiated

static QList<Base<?>> s_RegisteredClasses; // ? prevents polymorphism
class A : public Base<A>{ ... };
class B : public Base<B>{ ... };
/* Both A & B run `Register()` constructor,
   but how do you add them to a list? */

The self initialization works, registration doesn't.

Should I consider a QList<void *> instead?

The other variant looks like this, with the base class having one signature:

class Register { public: Register() { /* Static Constructor to run any code */ } };

template<class T>
class Base { static inline T selfRegister{} }; // Register initiated only once.

static QList<Base<Register>> s_RegisteredClasses; // but I can now polymorph
class A : public Base<Register>{ ... };
class B : public Base<Register>{ ... };
/* How can I self initialize A & B, adding them to s_RegisteredClasses ? */

I am close to giving up, but I haven't tried registering function pointers yet.

Is a text Macro the appropriate solution here, or that there is another paradigm I am overlooking?


Context

I am designing a Card Game where the instructions are printed in a Pseudo C++ code. I am hoping a subclass would represent a Card looking like this:

Was created a few years ago; might not use the same instructions today but the point is gotten across well enough.

12
  • 3
    You can't have a design that is decoupled for every kind of change imaginable; you should tell us what you're trying to do in more concrete terms. Also, "Definition is Registration" is not, AFAIK, a widely established term - but take a look at this. Another important thing: what do subclasses represent - different data types or different operations? (And what "functional variant" refers to in these terms?) Which of the two you're more likely to add to when you extend the code? May 2, 2020 at 19:39
  • 5
    "would you rather have 200 enumerators, or 200 header files" - I would rather have one header/class file (or maybe a couple of them) and 200 objects :) It's an interesting idea, though. The problem with virtual functions is that if you come up with a new idea for a card functionality that you didn't take into account before, you'll have to change the base class. Maybe this is better suited for something like an interpreter, or an embedded scripting language (represent each card as a script file, then load the scripts). May 2, 2020 at 20:14
  • 3
    One alternative approach to consider is to have the hierarchy model the operations (the pseudocode on the cards), rather then the cards themselves - and then have the cards be composed out of those (a small tree of some sort). That way you could both leverage compiler errors and reduce the number of things you'd need to define (assuming the card language is not itself so complex that this approach would require 200+ types as well). And adding new kinds of logic would be decoupled from creating new kinds of cards, too. May 2, 2020 at 21:26
  • 3
    Aside from scripting (which would appear like you would need to define your own language, with syntax, parser, and in-memory code-representation), you can also implement just enough dynamic behavior to approximate that. For example, Command Pattern can be used to represent the individual "actions" that a single line of scripting language would be capable of. Then, instead of having 200 classes, you could have 200 instances of a single Card class. (In other words, "behaviors driven by data.") You can initialize these 200 instances inside C++ code, no need for a separate language.
    – rwong
    May 2, 2020 at 23:03
  • 3
    @Akiva: I backup the other commenters. An approach with 200 subclasses is doomed, this will undoubtly end in a maintenance nightmare. And if you really need a design where the pseudo-instructions on the cards are written in the a subset of the implementation language and executed as part of the same run-time environment, you may consider to use a more dynamic programming language than C++. Alternatively, embed a scripting language like Lua into your program, Lua is actually designed for such purposes, and you find lots of tutorials on the web for this.
    – Doc Brown
    May 3, 2020 at 7:46

2 Answers 2

1

Use the power of C++, specifically, CRTP, to implement a registration mixin (Mixin-Based Programming with C++ (Smaragdakis, Batory)).

Your classes then don't even have to inherit (directly) from the same base class (as they would in an abstract class pattern). (Though they can from a base registration class that the CRTP class is derived from.) (Because part of the power of C++ is multiple inheritance of behavior, as well.)

namespace Details {
  class Base_Registrator { ... };
}

template <class C>
class Registrator
   : public Details::Base_Registrator {
   ... 
   ... { use C here to get useful stuff from it }
   ...
};

class MyCardishFoo 
   : public Whatever, 
     private Registrator<MyCardishFoo> {
};

[This answer in dedicated in loving memory to ATL: the greatest and most powerful and most ahead of its time C++ mixin library of all time.]

2
  • Why is Registrator inheriting from C? as it stands this is ill-formed
    – Caleth
    Feb 2, 2021 at 9:08
  • @Caleth - quite right I did that wrong, thanks! fixed now
    – davidbak
    Feb 2, 2021 at 15:07
0

You basically need a base class, a registration class, and a template class. From there, your derived classes will be neatly registered and defined without any extra effort on your part.

1: Abstract Base Class + Pointer List:

class Base
{
public:
    Base() {}
    virtual ~Base() {}
    virtual int supply() const = 0;
};
static QList<Base*> s_Registered;

Notes: Here, you should tightly define the functions you want used, preferably making them abstract to force your definitions in your derived class. supply() for example may be used by me to control the number of objects that can be created from this.

2: Static Constructor

class Registrar
{
public:
    Registrar( Base *c )
    {
        s_Registered << c;
    }
};

Notes: On definition of a class containing a static member for Registrar, its constructor will be called, giving you initialization on definition.

3: Template Class, Inheriting Base.

template<class T>
class Factory : public Base
{
public:
    Factory( int supply )
    {
        Factory<T>::s_Supply = supply;
        Q_UNUSED(s_Registrar)
    }
    int supply() const override
    {
        return s_Supply;
    }
private:
    static inline Registrar s_Registrar { new T() };
    static inline int s_Supply{0};
};

Notes: It is important for this to be a template, to give an inherently unique signature. This unique signature will ensure that a unique static Registrar will be created, hence running its constructor. Q_UNUSED(s_Registrar) is needed for RAII, ensuring the constructor will run. Thankfully this bit of ugly code can be hidden in this class which will be for the most part ignored here on out.

4: Your Derived Classes

class A : public Factory<A>
{
public:
    A() : Factory<A>(1337) {}
};
class B : public Factory<B>
{
public:
    B() : Factory<B>(0) {}
};

Notes: As long as you remember to public Factory<A> and : Factory<A>(1234), this derived class will register itself to QList, and when de-referenced, will call the functions defined here.

And that's that. Running my program,

Debug /home/akiva/Programming/Wiki/main.cpp:15
int main(int, char**)

int s_Registered.at(i)->supply()
int :: 1337

Debug /home/akiva/Programming/Wiki/main.cpp:15
int main(int, char**)

int s_Registered.at(i)->supply()
int :: 0
1
  • 1
    An Abstract Factory is a perfectly reasonable solution, yes. You don't really need to derive your concrete class from the generic factory, though, and you don't need to pre-allocate an instance of the concrete class either. It'd be more normal to register a Factory<A> object that implements a BaseFactory interface which can return Base*. The main thing to watch out for is the static initialization itself, which in some cases may be elided at link time, or behave differently in dynamically-linked programs etc. etc.
    – Useless
    May 5, 2020 at 12:21

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.