Search code examples
c++inheritancearchitectureembeddedreal-time

Efficient configuration of class hierarchy at compile-time


This question is specifically about C++ architecture on embedded, hard real-time systems. This implies that large parts of the data-structures as well as the exact program-flow are given at compile-time, performance is important and a lot of code can be inlined. Solutions preferably use C++03 only, but C++11 inputs are also welcome.

I am looking for established design-patterns and solutions to the architectural problem where the same code-base should be re-used for several, closely related products, while some parts (e.g. the hardware-abstraction) will necessarily be different.

I will likely end up with a hierarchical structure of modules encapsulated in classes that might then look somehow like this, assuming 4 layers:

Product A                       Product B

Toplevel_A                      Toplevel_B                  (different for A and B, but with common parts)
    Middle_generic                  Middle_generic          (same for A and B)
        Sub_generic                     Sub_generic         (same for A and B)
            Hardware_A                      Hardware_B      (different for A and B)

Here, some classes inherit from a common base class (e.g. Toplevel_A from Toplevel_base) while others do not need to be specialized at all (e.g. Middle_generic).

Currently I can think of the following approaches:

  • (A): If this was a regular desktop-application, I would use virtual inheritance and create the instances at run-time, using e.g. an Abstract Factory.

    Drawback: However the *_B classes will never be used in product A and hence the dereferencing of all the virtual function calls and members not linked to an address at run-time will lead to quite some overhead.

  • (B) Using template specialization as inheritance mechanism (e.g. CRTP)

    template<class Derived>
    class Toplevel  { /* generic stuff ... */ };
    
    class Toplevel_A : public Toplevel<Toplevel_A> { /* specific stuff ... */ };
    

    Drawback: Hard to understand.

  • (C): Use different sets of matching files and let the build-scripts include the right one

    // common/toplevel_base.h
    class Toplevel_base { /* ... */ };
    
    // product_A/toplevel.h
    class Toplevel : Toplevel_base { /* ... */ };
    
    // product_B/toplevel.h
    class Toplevel : Toplevel_base { /* ... */ };
    
    // build_script.A
    compiler -Icommon -Iproduct_A
    

    Drawback: Confusing, tricky to maintain and test.

  • (D): One big typedef (or #define) file

    //typedef_A.h
    typedef Toplevel_A Toplevel_to_be_used;
    typedef Hardware_A Hardware_to_be_used;
    // etc.
    
    // sub_generic.h
    class sub_generic {
        Hardware_to_be_used the_hardware;
        // etc.
    };
    

    Drawback: One file to be included everywhere and still the need of another mechnism to actually switch between different configurations.

  • (E): A similar, "Policy based" configuration, e.g.

    template <class Policy>
    class Toplevel { 
        Middle_generic<Policy> the_middle;
        // ...
    };
    
    // ...
    
    template <class Policy>
    class Sub_generic {
        class Policy::Hardware_to_be_used the_hardware;
        // ... 
    };
    
    // used as
    class Policy_A {
        typedef Hardware_A Hardware_to_be_used;
    };
    Toplevel<Policy_A> the_toplevel;
    

    Drawback: Everything is a template now; a lot of code needs to be re-compiled every time.

  • (F): Compiler switch and preprocessor

    // sub_generic.h
    class Sub_generic {
        #if PRODUCT_IS_A
            Hardware_A _hardware;
        #endif
        #if PRODUCT_IS_B
            Hardware_B _hardware;
        #endif
    };
    

    Drawback: Brrr..., only if all else fails.

Is there any (other) established design-pattern or a better solution to this problem, such that the compiler can statically allocate as many objects as possible and inline large parts of the code, knowing which product is being built and which classes are going to be used?


Solution

  • First I would like to point out that you basically answered your own question in the question :-)

    Next I would like to point out that in C++

    the exact program-flow are given at compile-time, performance is important and a lot of code can be inlined

    is called templates. The other approaches that leverage language features as opposed to build system features will serve only as a logical way of structuring the code in your project to the benefit of developers.

    Further, as noted in other answers C is more common for hard real-time systems than are C++, and in C it is customary to rely on MACROS to make this kind of optimization at compile time.

    Finally, you have noted under your B solution above that template specialization is hard to understand. I would argue that this depends on how you do it and also on how much experience your team has on C++/templates. I find many "template ridden" projects to be extremely hard to read and the error messages they produce to be unholy at best, but I still manage to make effective use of templates in my own projects because I respect the KISS principle while doing it.

    So my answer to you is, go with B or ditch C++ for C