LoveDaOOP LoveDaOOP - 3 months ago 48
C Question

Design pattern: C++ Abstraction Layer

I'm trying to write an abstraction layer to let my code run on different platforms. Let me give an example for two classes that I ultimately want to use in the high level code:

class Thread
{
public:
Thread();
virtual ~Thread();

void start();
void stop();

virtual void callback() = 0;
};

class Display
{
public:
static void drawText(const char* text);
};


My trouble is: What design pattern can I use to let low-level code fill in the implementation?
Here are my thoughs and why I don't think they are a good solution:


  1. In theory there's no problem in having the above definition sit in
    highLevel/thread.h
    and the platform specific implementation sit in
    lowLevel/platformA/thread.cpp
    . This is a low-overhead solution that is resolved at link-time. The only problem is that the low level implementation can't add any member variables or member functions to it. This makes certain things impossible to implement.

  2. A way out would be to add this to the definition (basically the Pimpl-Idiom):

    class Thread
    {
    // ...
    private:
    void* impl_data;
    }


    Now the low level code can have it's own struct or objects stored in the void pointer. The trouble here is that its ugly to read and painful to program.

  3. I could make
    class Thread
    pure virtual and implement the low level functionality by inheriting from it. The high level code could access the low level implementation by calling a factory function like this:

    // thread.h, below the pure virtual class definition
    extern "C" void* makeNewThread();

    // in lowlevel/platformA/thread.h
    class ThreadImpl: public Thread
    { ... };

    // in lowLevel/platformA/thread.cpp
    extern "C" void* makeNewThread() { return new ThreadImpl(); }


    This would be tidy enough but it fails for static classes. My abstraction layer will be used for hardware and IO things and I would really like to be able to have
    Display::drawText(...)
    instead of carrying around pointers to a single
    Display
    class.

  4. Another option is to use only C-style functions that can be resolved at link time like this
    extern "C" handle_t createThread()
    . This is easy and great for accessing low level hardware that is there only once (like a display). But for anything that can be there multiple times (locks, threads, memory management) I have to carry around handles in my high level code which is ugly or have a high level wrapper class that hides the handles. Either way I have the overhead of having to associate the handles with the respective functionality on both the high level and the low level side.

  5. My last thought is a hybrid structure. Pure C-style
    extern "C"
    functions for low level stuff that is there only once. Factory functions (see 3.) for stuff that can be there multiple times. But I fear that something hybrid will lead to inconsistent, unreadable code.



I'd be very grateful for hints to design patterns that fit my requirements.

Answer

You seem to want value semantics for your Thread class and wonder where to add the indirection to make it portable. So you use the pimpl idiom, and some conditional compilation.
Depending on where you want the complexity of your build tool to be, and if you want to keep all the low level code as self contained as possible, You do the following:

In you high level header Thread.hpp, you define:

class Thread
{
  class Impl:
  Impl *pimpl; // or better yet, some smart pointer
public:
  Thread ();
  ~Thread();
  // Other stuff;
};

Than, in your thread sources directory, you define files along this fashion:

Thread_PlatformA.cpp

#ifdef PLATFORM_A

#include <Thread.hpp>

Thread::Thread()
{
  // Platform A specific code goes here, initialize the pimpl;
}

Thread::~Thread()
{
  // Platform A specific code goes here, release the pimpl;
}

#endif

Building Thread.o becomes a simple matter of taking all Thread_*.cpp files in the Thread directory, and having your build system come up with the correct -D option to the compiler.

Comments