Platform abstraction using inheritance and factories

Started by
12 comments, last by _the_phantom_ 13 years, 3 months ago
I'm designing a low-level platform abstraction library (simply for educational purposes), and I'd like to run off my intended plan for how to model it whilst achieving complete transparency to the user.


class Time
{
public:
virtual long GetTicks() = 0;

static Time* Create()
{
#if defined _WIN32
return new TimeWin32;
#elif defined _LINUX
return new TimeLinux;
#endif
}
};

class TimeWin32
{
public:
long GetTicks() { return SomeWin32GetTimeFunction(); }
};

class TimeLinux
{
public:
long GetTicks() { return SomeLinuxGetTimeFunction(); }
};

//Usage
Time* time = Time::Create();
long CurrTicks = time->GetTicks();


Obviously that wouldn't build off the bat, but you get the idea.

I've done a bit of research, and seen methods like creating a unified header, and putting the function bodies into platform specific .cpp files, then #including the correct ones into the project, but that seems very C-like to me.

Do you have a preferred method?

"The right, man, in the wrong, place, can make all the dif-fer-rence in the world..." - GMan, Half-Life 2

A blog of my SEGA Megadrive development adventures: http://www.bigevilcorporation.co.uk

Advertisement

I've done a bit of research, and seen methods like creating a unified header, and putting the function bodies into platform specific .cpp files, then #including the correct ones into the project, but that seems very C-like to me.

This is how it is generally done in C++, where such an approach is possible. It can be difficult in cases where different platforms have different API models, e.g. a three step process vs a two step process. You have to look at the common denominator and design your system like that, or try to hide the complexity. In fact, the specific example here would just use a free function, not a class at all. Hybrid approaches can work too.


In other cases, a COM style approach is used where the implementation is dynamically linked via a DLL or shared library. I suppose the thinking is that if you are going to pay the virtual costs at runtime, then you might as well get the benefits of dynamically replacing the functionality. Remember that modern C++ is a strongly value oriented language, unnecessary inheritance and dealing with raw pointers run contrary to this.
long GetTime()
{
#if defined(MY_WINDOWS_DEFINE)
return getWindowsTime();
#elif defined(MY_LINUX_DEFINE)
return getLinuxTime();
#else
#error Invalid platform
#endif
}



Fast, simple, and clean; no need for a class abstraction. In some rare cases it's worth doing something complex like you describe (e.g. if you have a multi-phase process that needs to retain state) but usually platform-specifics can be hidden fairly easily, at least unless you're doing something arcane.

Wielder of the Sacred Wands
[Work - ArenaNet] [Epoch Language] [Scribblings]

The way I see it, you're adding [font="Courier New"]virtual function calls for no reason, other than inheritance-addiction.
[Edit - as ApochPiQ posts above, you're also using [font="Courier New"]new and [font="Courier New"]class for no reason ;)]

IMO, equating "The C++ way" with "solving everything via inheritance" is a very 90's way of looking at C++, and is what lead us to Java's OO-abusing design... Inheritance should be used very sparingly in a good OO design.

I'd much prefer to use compile-time polymorphism here instead of runtime polymorphism. After all, you're never going to instantiate both a [font="Courier New"]TimeWin32 and a [font="Courier New"]TimeLinux in the same build.
That looks like complexity for complexity's sake. I do this in my project (very generalized code), which you've already described, but to me makes things much easier to look at and work with:


ifdef WIN32
#include "w32_Time.h"
// other windows includes

ifdef LINUX
#include "lin_Time.h"
// other linux includes


// somewhere in main code
Time time;
time.GetTime();



w32_Time.h


class Time {

void GetTime() { // get time for win32 }
};


lin_Time.h


class Time {

void GetTime() { // get time for linux }
};

The way I see it, you're adding [font="Courier New"]virtual function calls for no reason, other than inheritance-addiction.
[Edit - as ApochPiQ posts above, you're also using [font="Courier New"]new and [font="Courier New"]class for no reason ;)]


True for the example I gave, but eventually I'm gearing towards larger systems - an audio subsystem, for example. I'd need functionality to enumerate and initialise audio hardware, as well a voice wrapper to play back audio buffers, and several different filetype readers, not to mention a consistent interface for adjusting volume arrays, creating and connecting busses, reverb and filter effects. All of that requires a platform specific implementation, and then to add in the common base functionality - 3D positioning routines, falloff routines, software effects. That's one big system, and creating an effective design AND maintaining consistency seems a lot easier to maintain with a class hierarchy, in my mind.

I suppose inheritance-addiction could be the correct term, but it's how I learned C++ and it seems an invaluable tool for designing systems like this. Cost of virtual functions and vtables asside, from a DESIGN perspective, does my example hold water? I'm sceptical when people mention the speed of these things - it's hard to find a discussion that actually shows test figures for exactly HOW slow the cost of virtualision is, and most threads on the matter simply end with "just write the damn thing, worry about optimisation when it becomes a problem!".


That looks like complexity for complexity's sake. I do this in my project (very generalized code), which you've already described, but to me makes things much easier to look at and work with:


ifdef WIN32
#include "w32_Time.h"
// other windows includes

ifdef LINUX
#include "lin_Time.h"
// other linux includes


// somewhere in main code
Time time;
time.GetTime();




Again, I have no problem with that for the small Get Time example, but try abstracting a renderer like that and it'll end up an unbearable mess. Maybe I could expand on my original question and the example code, I'll give it a shot when I get home.

Thanks for the brain food so far!

"The right, man, in the wrong, place, can make all the dif-fer-rence in the world..." - GMan, Half-Life 2

A blog of my SEGA Megadrive development adventures: http://www.bigevilcorporation.co.uk

I suggest using a single header. Use #ifdef guards around platform-specific class members (private variables and the like--things that don't affect the platform-independent public interface). Then, create platform-specific implementation files. If not all class functionality is platform-specific, you could even add a generic implementation file that contained all of the platform-independent stuff.

Time.h
class Time
{
private:
long mIndependentVariable;
#ifdef WIN32
int mWindowsDependentVariable;
#else
int mLinuxDependentVariable;
#endif

public:
long IndependentFunction();
long DependentFunction();
};


Time.cpp
long Time::IndependentFunction()
{
return 0;
}


Windows/Time.cpp
long Time::DependentFunction()
{
return 0;
}


Linux/Time.cpp
long Time::DependentFunction()
{
return 0;
}


C++ is not like Objective-C. You can spread your class implementation across as many files as you want (though, you can do that in Objective-C with the use of categories--but that's not the point).
Put your interface in the header file. One single, platform-independant interface in one single header file.

Put your platform-dependant implementation in a separate .cpp source file. Build and link only the relevant implementation.

If necessary, use the pimpl idiom to hide implementation details in the header file.

That is the C++ way.

Stephen M. Webb
Professional Free Software Developer


long GetTime()
{
#if defined(MY_WINDOWS_DEFINE)
return getWindowsTime();
#elif defined(MY_LINUX_DEFINE)
return getLinuxTime();
#else
#error Invalid platform
#endif
}



Fast, simple, and clean; no need for a class abstraction. In some rare cases it's worth doing something complex like you describe (e.g. if you have a multi-phase process that needs to retain state) but usually platform-specifics can be hidden fairly easily, at least unless you're doing something arcane.


Same argumentation, but I find this even more clean and less writing:


#ifdef WINDOWS
long GetTime {
// some windows specific calls here
}
#elif LINUX
long GetTime {
// some linux specific calls here
}
#else
#error meh
#endif


That is, for small functions like this. On a case by case decision, "inner cluttering" might be preferable. But imho, ideally, a function should be refactored then so it does not have inner cluttering.

Best of all is perhaps to use boost or Qt (or another cross platform api of your choice) for this.
After a bit more research and sleep, the factory implementation I used above does seem a bit daft. I've thought about using typedefs, see below.


That is, for small functions like this. On a case by case decision, "inner cluttering" might be preferable. But imho, ideally, a function should be refactored then so it does not have inner cluttering.


So how would you refactor it? Let's use an example a little more complex than a timekeeping function - low-level GUI functionality for Windows, Mac and KDE perhaps - now there's a lot to think about. Somehow we'd need to abstract the creation of a window, keyboard and mouse input, and some basic rendering. Here's something to start off:


class Window
{
public:
Window(const std::string& Title, const Vector2& Dimentions, const Vector2& Position);
virtual ~Window();

void Update(float DeltaTime);

bool HasFocus();
virtual void OnMove() {};
virtual void OnClose() {};
virtual void OnMinimise() {};
virtual void OnMaximise() {};
Vector2 GetMouseAbsCoords();

private:
#if defined _WINDOWS
HWND mWindowHandle;
#elif defined _MACOSX
MacWindow mWindow;
#elif defined _KDE
KDEWindow mWindow;
#endif

bool mHasFocus;
};


Ideally, the user of the library would derive from this class, add their implementation for the OnMove, OnClose callbacks, etc, and it would be their responsibility to call Update periodically, passing in a valid delta. Under the hood, I'd be tempted to have Window.cpp for any generic functionality (HasFocus), and then Window_W32.cpp, Window_KDE.cpp and Window_Mac.cpp containing the constructor, Update and GetMouseCoords bodies.

Now, here's where it gets tricky - what if one platform used a polling mechanism for fetching the current position, and another used an event to set new coords? The latter would mean some extra #ifdefs in the header to store a Vector2 for the current position as and when it changes. Let's say this list of extra #ifdefs grew until the point it gets messy, and we decide to refactor. We'd have a lot of generic variables which are required for all platforms, and each platform implementation required a lot of variables for itself. How about inheritance and typedefs?


class WindowBase
{
public:
Window(const std::string& Title, const Vector2& Dimentions, const Vector2& Position);
virtual ~Window();

virtual void Update(float DeltaTime);

bool HasFocus();
virtual void OnMove();
virtual void OnClose();
virtual void OnMinimise();
virtual void OnMaximise();
virtual Vector2 GetMouseAbsCoords();

private:

//Lots of generic variables for the base implementation
bool mHasFocus:
};

class WindowKDE : public WindowBase
{
public:
WindowKDE(const std::string& Title, const Vector2& Dimentions, const Vector2& Position);
virtual ~WindowKDE();

void Update(float DeltaTime);
Vector2 GetMouseAbsCoords();

private:
//Event-based update callback for this platform
void OnEvent(int EventId, void* EventParams);

//Lots of platform specific variables
KDEWindow mWindow;
};

#if defined _WINDOWS

#include "WindowW32.h"
typedef WindowW32 Window;

#elif defined _MACOSX

#include "WindowMac.h"
typedef WindowMac Window;

#elif defined _LINUX

#include "WindowKDE.h"
typedef WindowKDE Window;

#endif


The usage would remain the same, yet everything would be tidier, the platform #ifdefs are absolutely minimal, and we could make good use of the inheritance hierarchy when calling the Update() function down through each layer.

Your thoughts?


Best of all is perhaps to use boost or Qt (or another cross platform api of your choice) for this.


This is just a learning excercise, wxWidgets is my weapon of choice for anything serious.

"The right, man, in the wrong, place, can make all the dif-fer-rence in the world..." - GMan, Half-Life 2

A blog of my SEGA Megadrive development adventures: http://www.bigevilcorporation.co.uk

This topic is closed to new replies.

Advertisement