views:

455

answers:

8

I'm having a bit of a go at developing a platform abstraction library for an application I'm writing, and struggling to come up with a neat way of separating my platform independent code from the platform specific code.

As I see it there are two basic approaches possible: platform independent classes with platform specific delegates, or platform independent classes with platform specific derived classes. Are there any inherent advantages/disadvantages to either approach? And in either case, what's the best mechanism to set up the delegation/inheritance relationship such that the process is transparent to a user of the platform independent classes?

I'd be grateful for any suggestions as to a neat architecture to employ, or even just some examples of what people have done in the past and the pros/cons of the given approach.

EDIT: in response to those suggesting Qt and similar, yes I'm purposely looking to "reinvent the wheel" as I'm not just concerned with developing the app, I'm also interested in the intellectual challenge of rolling my own platform abstraction library. Thanks for the suggestion though!

+3  A: 

Have a look at ACE. It has a pretty good abstraction using templates and inheritance.

Amit Kumar
+8  A: 

I'm using platform neutral header files, keeping any platform specific code in the source files (using the PIMPL idiom where neccessary). Each platform neutral header has one platform specific source file per platform, with extensions such as *.win32.cpp, *.posix.cpp. The platform specific ones are only compiled on the relevent platforms.

I also use boost libraries (filesystem, threads) to reduce the amount of platform specific code I have to maintain.

It's platform independent classes declarations with platform specific definitions.

Pros: Works fairly well, doesn't rely on the preprocessor - no #ifdef MyPlatform, keeps platform specific code readily identifiable, allows compiler specific features to be used in platform specific source files, doesn't pollute the global namespace by #including platform headers.

Cons: It's difficult to use inheritance with pimpled classes, sometimes the PIMPL structs need their own headers so they can be referenced from other platform specific source files.

Joe Gauterin
To throw in some search terms for further reading: I think this is also called "Bridge Pattern" or "Implementation Pattern".
mxp
interesting, this is what I've been doing. I have a question, what If like you have 2 platforms where 90% of the code is the same between platforms for that class/file?
matt
@matt: You can use a common source file; foo.h (platform independent), foo.cpp (containing common code), foo.win32.cpp, foo.posix.cpp. Depending on what foo.cpp contains, it can either be compiled seperately then linked, or just #included into foo.win32.cpp and foo.posix.cpp
Joe Gauterin
I'd have 3 implementation files in that case, if it's a big class. Like... window.cpp, window.win32.cpp, and window.posix.cpp. If it's a smaller class (or small deltas), just use #ifdefs. However, be perfectly ready to refactor a little if the #ifdefs start to add up and obscure your design.
darron
Sounds like this might be the way to go. I'll do some tinkering and report back... Thanks for the suggestion Joe!
Mac
I don't see why Inheritance should be an issue. After all the inner workings of the class I derive from are of no concern to me, only the operations are worth considering.
Matthieu M.
This is the way I've gone. Kudos to mxp - the Wikipedia article for "Bridge Pattern" describes what I've done almost exactly. @Matthieu: Based on my limited experience with PIMPL, the problem seems to be when the derived class needs to add its own member variables - things can get messy since you then need to also subclass the PIMPL structure, casts start littering your code, etc. Doable, but potentially hard to keep track of.
Mac
Ah! That's not the way to go! Each class is responsible for its own attributes. You should not modify the Impl of the base class for the benefit of the derived, instead the derived class should have its own attributes (perhaps in a pimpl too) and its own factory if necessary (to decide how to build it depending on the architecture). Of course it may seem like doubling the code, but with a glimpse of templates, the factory code is factorized right away.
Matthieu M.
+3  A: 

Another way is to have platform independent conventions, but substitute platform specific source code at compile time.

That is to say that if you imagine a component, Foo, that has to be platform specific (like sockets or GUI elements), but has these public members:

class Foo {
public:
  void write(const char* str);
  void close();
};

Every module that has to use a Foo, obviously has #include "Foo.h", but in a platform specific make file you might have -IWin32, which means that the compiler looks in .\Win32 and finds a Windows specific Foo.h which contains the class, with the same interface, but maybe Windows specific private members etc.

So there is never any file which contains Foo as written above, but only sets of platform specific files which are only used when selected by a platform specific make file.

quamrana
A: 

You might also want to take a look at poco:

The POCO C++ Libraries (POCO stands for POrtable COmponents) are open source C++ class libraries that simplify and accelerate the development of network-centric, portable applications in C++. The libraries integrate perfectly with the C++ Standard Library and fill many of the functional gaps left open by it. Their modular and efficient design and implementation makes the POCO C++ Libraries extremely well suited for embedded development, an area where the C++ programming language is becoming increasingly popular, due to its suitability for both low-level (device I/O, interrupt handlers, etc.) and high-level object-oriented development. Of course, the POCO C++ Libraries are also ready for enterprise-level challenges.

poco architecture

none
+1  A: 

I might go for a policy-type thing:

template<typename Platform>
struct PlatDetails : private Platform {
    std::string getDetails() const {
        return std::string("MyAbstraction v1.0; ") + getName();
    }
};

// For any serious compatibility functions, these would
// of course have to be in different headers, and the implementations
// would call some platform-specific functions to get precise
// version numbers. Using PImpl would be a smart idea for these 
// classes if they need any platform-specific members, since as 
// Joe Gauterin says, you want to avoid your application code indirectly
// including POSIX or Windows system headers, containing useless definitions.
struct Windows {
    std::string getName() const { return "Windows"; }
};

struct Linux {
    std::string getName() const { return "Linux"; }
};

#ifdef WIN32
    typedef PlatDetails<Windows> PlatformDetails;
#else
    typedef PlatDetails<Linux> PlatformDetails;
#endif

int main() {
    std::cout << PlatformDetails().getName() << "\n";
}

There's not a whole lot to choose though between doing this, and doing regular simulated dynamic binding with CRTP, so that the generic thing is the base and the specific thing the derived class:

template<typename Platform>
struct PlatDetails {
    std::string getDetails() const {
        return std::string("MyAbstraction v1.0; ") + 
            static_cast<Platform*>(this)->getName();
    }
};

struct Windows : PlatDetails<Windows> {
    std::string getName() const { return "Windows"; }
};

struct Linux : PlatDetails<Linux> {
    std::string getName() const { return "Linux"; }
};

#ifdef WIN32
    typedef Windows PlatformDetails;
#else
    typedef Linux PlatformDetails;
#endif

int main() {
    std::cout << PlatformDetails().getName() << "\n";
}

Basically in the latter version, getName must be public (although I think you can use friend) and so must be the inheritance, whereas in the former, the inheritance can be private and/or the interface functions can be protected, if desired. So the adaptor can be a firewall between the interface the platform has to implement, and the interface your application code uses. Furthermore you can have multiple policies in the former (i.e. multiple platform-dependent facets used by the same platform-independent class), but not for the latter.

The advantage of either of them over versions with delegates or non-template-using inheritance, is that you don't need any virtual functions. Arguably this isn't a whole lot of advantage, considering how scary both policy-based design and CRTP are at first contact.

In practice, though, I agree with quamrana that normally you can just have different implementations of the same thing on different platforms:

// Or just set the include path with -I or whatever
#ifdef WIN32
    #include "windows/platform.h"
#else
    #include "linux/platform.h"
#endif

struct PlatformDetails {
    std::string getDetails() const {
        return std::string("MyAbstraction v1.0; ") + 
            porting::getName();
    }
};

// windows/platform.h
namespace porting {
    std::string getName() { return "Windows"; }
}

// linux/platform.h
namespace porting {
    std::string getName() { return "Linux"; }
}
Steve Jessop
heh, just as long as Linux and Windows are your only two platforms :)I understand this is just a simple example, but the pain it has caused using the 'ifdef platform else' when deciding we need another platform could have easily been avoided. So if using ifdefs, I would suggest you don't assume that you won't add any more platforms, do #if defined plat1 #elif defined plat2 #else #error #endif
matt
Sure. Whether there's one of those ifdef things per component of the compatibility layer, or just one big one that sets everything up, it should positively identify every platform rather than be lazy about the last one as I have. One of the advantages of using a platform-dependent include path is not having to do any such ifdeffery.
Steve Jessop
A: 

If you like to use a full-blown c++ framework available for many platforms and permissive copylefted, use Qt.

henchman
A: 

So... you don't want to simply use Qt? For real work using C++, I'd very highly recommend it. It's an absolutely excellent cross-platform toolkit. I just wrote a few plugins to get it working on the Kindle, and now the Palm Pre. Qt makes everything easy and fun. Downright rejuvenating, even. Well, until your first encounter with QModelIndex, but they've supposedly realized they over-engineered it and they're replacing it ;)

As an academic exercise though, this is an interesting problem. As a wheel re-inventor myself, I've even done it a few times now. :)

Short answer: I'd go with PIMPL. (Qt sources have examples a-plenty)

I've used base classes and platform specific derived classes in the past, but it usually ends up a bit messier than I had in mind. I've also done part of an implementation using some degree of function pointers for platform specific bits, and I was even less happy with that.

Both times I ended up with a very strong feeling that I was over-architecting and had lost my way.

I found using private implementation classes (PIMPL) with different platforms specific bits in different files easiest to write AND debug. However... don't be too afraid of an #ifdef or two, if it's just a few lines and very clear what's going on. I hate cluttered or nested #ifdef logic, but one or two here and there can really help avoid code duplication.

With PIMPL, you're not constantly refactoring your design as you discover new bits that require different implementations between platforms. That way be dragons.

At the implementation level, hidden from the application... there's nothing wrong with a few platform specific derived classes either. If two platform implementations are fairly well defined and share almost no data members, they'd be a good candidate for that. Just do it after realizing that, not before out of some idea that everything needs to fit your selected pattern.

If anything, the biggest gripe I have about coding today is how easily people seem to get lost in idealism. PIMPL is a pattern, having platform specific derived classes is another pattern. Using function pointers is a pattern. There's nothing that says they're mutually exclusive.

However, as a general guideline... start with PIMPL.

darron
A: 

There're also the big boys, such as Qt4 (complete framework + GUI),GTK+ (gui-only afaik), and Boost (framework only, no GUI), all 3 support most platforms, GTK+ is C, Qt4/Boost are C++ and for the most part template based.

OneOfOne