Cross-Platform C++ code and single header - multip

2020-05-17 10:59发布

问题:

I have heard that a way to write Cross Platform c++ code is to define classes as follows (for example, a Window class):

window.h
window_win32.cpp
window_linux.cpp
window_osx.cpp

and then choose the implementation file accordingly. But what if i have members of that class that are relative to the os? Like a HWND member for the Win32 implementation. I can't put it in the window.h or when i'd try to compile it on, say, Linux, it'd generate a compiler error.

Do i need to #ifdef it? I've already asked a similar question but this one is more focused on this particular problem.

回答1:

There is more ways to solve this problem - each has it's pros and cons.

1.) Use macros #ifdef, #endif

// Note: not sure if "WINDOWS" or "WIN32" or something else is defined on Windows
#ifdef WINDOWS
    #include <window.h>
#else
    // etc.
#endif

class MyClass
{
public:

    // Public interface...

private:

#ifdef WINDOWS
    HWND m_myHandle;
#else
    // etc.
#endif

};

Pros:

  • Maximal speed of program.

Cons:

  • Worse readibility. With many platforms it can get really messy.
  • Including platform specific includes might break something. windows.h defines many macros with normal names.

2.) As was there already written, you might use polymorphism:

// IMyClass.h for user of your class:

class IMyClass
{
public:

    virtual ~IMyClass() {}
    virtual void doSomething() = 0;

};


// MyClassWindows.h is implementation for one platform

#include <windows.h>
#include "IMyClass.h"

class MyClassWindows : public IMyClass
{
public:

    MyClassWindows();
    virtual void doSomething();

private:

    HWND m_myHandle;

};

// MyClassWindows.cpp implements methods for MyClassWindows

Pros:

  • Much, much more cleaner code.

Cons:

  • User cannot create instances of your class directly (especially not on stack).
  • You must provide special function for creation: for example declare IMyClass* createMyClass(); and define it in MyClassWindows.cpp and other platform specific files. In that case (well, in fact in this whole polymorphism case) you should also define function which destroys the instances - in order to keep idiom "whoever created it should also destroy".
  • Little slowdown because of virtual methods (in these days practically completely insignificant except very, very special cases).
  • Note: the allocation can be problem on platforms with limited memory because of problems with RAM fragmentation. In that case, it can be solved by using some kind of memory pool for your objects.

3.) PIMPL idiom.

// MyClass.h

class MyClass
{
public:

    MyClass();
    void doSomething();

private:

    struct MyClassImplementation;

    MyClassImplementation *m_impl;

}


// MyClassWindows.h

#include <windows.h>
#include "MyClass.h"

struct MyClassImplementation
{
    HWND m_myHandle;

    void doSomething();

}

In this case, MyClassImplementation keeps all needed (at least platform specific) data and implements what is needed (again, platform specific). In MyClass.cpp you include the platform specific implementation (methods can be inline), in constructor (or later if you want to - just be careful) you allocate the implementation and in destructor you will destroy it.

Pros:

  • User can create instances of your class (including on stack) (no worrying about un/deleted poiners).
  • You do not need to include platform specific headers in MyClass.h.
  • You can simply add reference counter and implement data sharing and/or copy-on-write which can easily allow to use your class as return value even if it keeps big amount of data.

Cons:

  • You must allocate implementation object. Object pool can help.
  • When calling a methods, two are called instead and one pointer dereferencing. Still, today shouldn't be any problem.

4.) Define a neutral type, which is big enough to keep your data. For example long long int.

// MyClass.h

class MyClass
{
public:

    MyClass();
    void doSomething();

private:

    typedef unsigned long long int MyData;

    MyData m_data;

};

In implementation (e.g. MyClassWindows.cpp) you always need to cast (reinterpret casting) between MyClass::MyData and actual data stored.

Pros:

  • As fast as first way but you avoid macros.
  • Avoiding allocation if not needed.
  • Avoiding multiple method calls.
  • Avoiding including platform specific headers in MyClass.h.

Cons:

  • You must be absolutely 110% sure that size of MyClass::MyData is always at least same as data stored.
  • If different platform stores differently sized data, you are waisting with space (if you use a few items, it's ok, but with millions of them...) unless you use macros. In this case, it won't be so messy.
  • It's low level work with data - unsafe, not OOP. But fast.

So use the one which is best fitting to your problem... and your personality :3 because with today's power are all four more or less relatively equal in terms of speed and space.



回答2:

I would only add a 5) bullet to @Laethnes' answer

5) Write an empty header that includes, at compile-time, the platform header and hide the platform-specific class under a typedef

// MyClass.hpp

#if defined(WINDOWS)
#    include "WINMyClass.hpp"
     typedef WINMyClass MyClass
#elif defined(OSX)
#    include "OSXMyClass.hpp"
     typedef OSXMyClass MyClass

   ...  // keep going

#endif

Pros:

  • it's only a typedef, very simple
  • good readibility
  • speed

Cons:

  • it's only a typedef


回答3:

The point of such an approach is, that you encapsulate OS specific data in the os specific file. If your have to pass around a HWND, then you might reconsider your object design. Wether such a strcuture makes sense depends on how big your os specific code is. You don't really want to squeeze all possible classes into a single file.

On the other hand, there are libraries for GUIs which are doing exactly this - encapsulating the OS specific parts in a library like QT or wxWidgets or others. If you properly separate the GUI from the main code, then you may not even need this approach.

I'm using such a structure in my project to support different versions of xerces, without having the maincode cluttering with #ifdefs. However in my case, the affected code is rather small. I can imagine that a GUI framework takes much more space.



回答4:

A common way to do this is to use polymorphism. You provide an interface that abstracts your functionality without any regard to a specific platform:

class thread
{
    virtual ~thread() {}
    virtual void run() = 0;
    /* whatever */
};

and then you can inherit from this class using platform specific features:

class posix_thread : thread;

at compile time you choose with #ifdefs what class you include and instantiate.



回答5:

Most of the time, you avoid such things in the header. There are times, however, when you want to expose them. In such cases, my solution has always been to use something like:

#include dependentInclude(syst,MyInclude.lhh)

dependentInclude being a macro which expands to the the path of the file I need, depending on the value of syst (which was set on the command line).