Question

I am a writing a program which aims to be cross-platform; as such, it will support multiple implementations of certain operations. My first idea was to write a hierarchy of classes, with a common interface, maybe an abstract factory for each platform.

class Operation {
    DoOperation() = 0;
}

class OperationPlatform1 : public Operation {
    DoOperation();
}

class OperationPlatform2 : public Operation {
    DoOperation();
}

#ifdef USING_PLATFORM1
    Operation *op = new OperationPlatform1;
#endif

I realized, however, that the implementation that will be used is known at compile-time. I tried to think how I could implement this using static polymorphism, after which I realized that I could also write something along these lines:

class OperationPlatform1 {
    DoOperation();
}

class OperationPlatform2 {
    DoOperation();
}

#ifdef USING_PLATFORM1
typedef OperationPlatform1 Operation;
#endif

Operation op;

What is a good way to abstract multiple implementations, of which only one will be selected at compile-time? I am interested in performance as well, so I would like to not use virtual methods unless I have to.

Was it helpful?

Solution

The usual solution is to define just the one class in the header, and provide different source files for it, compiling and linking whichever is necessary. If you need some dependencies (e.g. data types) in class definition, you can also achieve this, by creating a specific header file for each platform, and including it. The simplest solution here is probably to create a directory per target platform, and put all of the platform dependent files there, using -I//I to pick up the correct directory (and thus the correct platform dependent files) at compile time.

Excessive use of #ifdef is usually a sign of poor design. See https://www.usenix.org/legacy/publications/library/proceedings/sa92/spencer.pdf.

OTHER TIPS

You've got a few options:

  1. Use the approach outlined in your question. However, chances are you'll need to split the platform specific bits into seperate files and use a build rule to bring in the correct file for the platform.

  2. Just have one class, and use #ifdef to insert the correct code for the appropriate platform.

  3. Use a pre-existing wrapper library (eg Boost) which may already wrap up the platform specific bits, and code against the wrapper.

If you're going to be encapsulating various platforms then you'll need to make sure that the abstractions don't leak through into your implementation. Often you'll end up using a combination of the three items listed.

What you have here is commonly used. If you absolutely know you'll never have multiple implementation at the same time, you can use a single header file and either have conditionally compiled implementation file(cpp) or have platform precompiler directive to exclude some code(other platforms) from compilation.

You could have in your header:

class Operation {
    DoOperation();
}

And multiple cpp in you can exclude files from your build:

Platform1.cpp

Operation::DoOperation() { // Platform 1 implementation }

and Platform2.cpp

Operation::DoOperation() { // Platform 2 implementation }



Or a single implementation file:

#ifdef PLATFORM1
    Operation::DoOperation() { // Platform 1 implementation }
elseif defined(PLATFORM2)
    Operation::DoOperation() { // Platform 2 implementation }
#endif



Or a mix of the 2 if you don't want to mess with exluding files from build:

Platform1.cpp:

#ifdef PLATFORM1
    Operation::DoOperation() { // Platform 1 implementation }
#endif

and Platform2.cpp:

#ifdef PLATFORM2
    Operation::DoOperation() { // Platform 2 implementation }
#endif

What you want is providing support for different platforms while keeping your source code clean. Therefore you should abstract the platfom dependencies behing some interface like your first idea does it.

At the same time you might want to use #ifdef for the implementation. As an example, file system functions very much depend on the platform, so you would define a file system interface like this.

class IFileSystem {
public:
    void CopyFile(const string& destName, const string& sourceName);
    void DeleteFile(const string& name);
    // etc.
};

Then you could have implementations for WindowsFileSystem, LinuxFileSystem, SolarisFileSystem etc.

Now when your source code requires file system functionality, you would access the requested interface like this:

IFileSystem& GetFileSystem() {
   #ifdef WINDOWS
       return WindowsFileSystemInstance;
   #endif
   #ifdef LINUX
       return LinuxFileSystemInstance;
   #endif
   // etc.
}

GetFileSystem().CopyFile("dest", "source");

This approach helps you separating concerns of application logic and platform dependencies.

The additional benefit of the getter function is that you still have the possibility to do runtime choices for the file system implementation, e.g. Fat32FileSystem, NtfsFileSystem, Ext3FileSystem etc.

Take a look at existing multiplatform frameworks, Qt for example. Use PIMPL idiom

Licensed under: CC-BY-SA with attribution
Not affiliated with StackOverflow
scroll top