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.

Anon
  • 3,565
  • 3
  • 27
  • 45
  • 2
    "200 subclasses" - that's feels too many, doesn't seem like a good idea, especially not from the get-go. "Decoupled" - that would be ideal, but note that you may need several iterations before you get the structure that (mostly or completely) supports this for the kinds of changes that you need supported. So don't go in assuming that you'll get the design right before you even start. Also, how you go about this depends *entirely* on the kind of problem you're trying to solve (the domain, the kinds of changes you expect), and you haven't written anything about that. – Filip Milovanović May 02 '20 at 19:38
  • 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](https://stackoverflow.com/questions/10332725/how-to-automatically-register-a-class-on-creation). 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? – Filip Milovanović May 02 '20 at 19:39
  • @FilipMilovanović would you rather have 200 enumerators, or 200 header files? I like the header files (subclasses) because I can just put them off to the side and forget about them. My usecase also benefits, because documentation is important, and I want to update it as little as possible. What I am building in this case, is a Card Game similar to `Magic the Gathering`. The caveat, is that the card instructions will be written in a psuedo c++ code, as its intended to help people learn to code. I plan to opensource this code along side of it. – Anon May 02 '20 at 19:48
  • @FilipMilovanović My base `class` is a Card with various virtual functions. `Functional Variant` just means, `How this card works when played in a game`. My subclasses will represent cards, like you would find in `Magic the Gathering`, and are thus how each one is played will often be quite complex. If things go my way, basically I can use the instructions put into a `virtual void play()` function can also be printed on literal cards and read by players. Documentation will be provided that explain programming principles behind it. – Anon May 02 '20 at 19:55
  • I'd expect a lot of similarity with card behaviors. Would having a different class for each card result in a lot of duplicated code (or near duplicated code that can be generalized to a couple of function parameters)? – 1201ProgramAlarm May 02 '20 at 20:07
  • 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). – Filip Milovanović May 02 '20 at 20:14
  • @1201ProgramAlarm For basic cards, yes it would. If you can imagine like a basic kiddie starters deck, its going to have extremely rudimentary instructions, like `++coins; buys++;` etc. However, the complexity will go deep, and I will design cards that will try and demonstrate different programming paradigms, and the whole set of features in C++ as well as a bunch of classes from Qt. – Anon May 02 '20 at 20:16
  • @FilipMilovanović Actually the main point of the virtual functions (if there is a better way to do this, please tell me), is so I do not forget to define things for 200+ cards. Ideally I would like to have a compile time check which an abstract class would achieve, but thats probably not possible. – Anon May 02 '20 at 20:22
  • 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. – Filip Milovanović May 02 '20 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 02 '20 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 03 '20 at 07:46
  • Use the power of C++. Specifically CRTP - to implement a registration Mixin. – davidbak Feb 02 '21 at 05:34

2 Answers2

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

davidbak
  • 712
  • 1
  • 7
  • 10
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<Base*>, and when de-referenced, will call the functions defined here.

And that's that. Running my program,

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

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

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

int s_Registered.at(i)->supply()
int :: 0
Anon
  • 3,565
  • 3
  • 27
  • 45