Cross platform C++ code architecture

2019-03-08 11:34发布

问题:

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!

回答1:

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.



回答2:

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.



回答3:

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



回答4:

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"; }
}


回答5:

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 http://pocoproject.org/images/poco.png



回答6:

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



回答7:

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.



回答8:

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.