We'll take a very simple file I/O subsystem here as an example (open, close). We'll also only take Linux and Windows, since that's usually the 2 most common platforms people want to develop for[1].

The usual abstraction with the preprocessor

Usually preprocessor #ifdefs are used to compile specific parts for the underlying OS.

The file fileiosys.cpp looks[2] like the one below :

fileiosys_filedesc open(char* filename) {
#ifdefine __WIN32__
    return OpenFile(
        filename,
        GENERIC_READ | GENERIC_WRITE,
        FILE_SHARE_READ | FILE_SHARE_WRITE,
        NULL,
    );
#else // UNIX
    return open(filename, O_READ | O_WRITE, 0666);
#endif
}

int close (fileiosys_filedesc fd) {
#ifdefine __WIN32__
    return CloseHandle(fd) ? 0 : -1;
#else // UNIX
    return close(fd);
#endif
}

The header fileiosys.h looks then like :

// Define a custom file descriptor
#ifdefine __WIN32__
    #define fileiosys_filedesc HANDLE
#else // UNIX
    #define fileiosys_filedesc int
#endif

fileiosys_filedesc open(char* filename);
int close(fileiosys_filedesc fd);

Avoid the preprocessor Compile-time polymorphism

As the previous little example, the code isn't really easy to read. You have to always think which environnement you are in. All the specifics are multiplexed in the same file, and the programmer has to always demultiplex it in real time each time he reads the code.

The last part of the interface file is quite simple to read since it's already abstracted away. Now let's completely demultiplex the implementation in several implementation files.

The header file is the common interface

The interface is the most important part of the design. It should be high-level enough to mask the differences between the plateforms you want to support, but not too high-level, otherwise you'll end up duplicating to much code.

So, for our file I/O subsystem, we'll just abstract the usual syscalls open, close in the same way as before.

The parameters that are passed through the interface are also very important. You cannot usually leak a platform-specific structure. So here all the file descriptors are just an opaque handle, represented by a pointer to a structure defined only as a forward declaration in the header. This pattern, sometimes called the pimpl idiom, enables use to really share the representation while implementing it differently.

The header file is preserved as fileiosys.h, whereas the implementation is in the files linux/fileiosys.cpp and win32/fileiosys.cpp

fileiosys.h

// Define the forward declaration
struct fis_filedesc;
fis_filedesc* open(char* filename);
int close(fileiosys_filedesc* fd);

win32/fileiosys.cpp

struct fis_filedesc {
    HANDLE handle;
};

fis_filedesc* open(char* filename) {
    fis_filedesc* fd = new fis_filedesc();
    fd->handle = OpenFile(
        filename,
        GENERIC_READ | GENERIC_WRITE,
        FILE_SHARE_READ | FILE_SHARE_WRITE,
        NULL,
    );

    return fd;
}

int close (fis_filedesc* fd) {
    int retval = CloseHandle(fd->handle) ? 0 : -1;
    delete(fd);
    return retval;
}

linux/fileiosys.cpp

struct fis_filedesc {
    int file_descriptor;
};

static fis_filedesc[32];

fis_filedesc* open(char* filename) {
    fis_filedesc* fd = new fis_filedesc();
    fd->file_descriptor = open(filename, O_READ | O_WRITE, 0666);
    return fd;
}

int close (fis_filedesc* fd) {
    int retval = close(fd);
    delete(fd);
    return retval;
}

The build system : Makefile

The makefile should take into account the different platforms, and only compile the needed implementation file. All the gluing magic will then be done at linking time instead of preprocessor time.

Conclusion

The interest of have multiple implementation files is obvious. It is much more straightforward to read and only marginally harder to write. But since most of the time code is read and not written, the choose is quite a no-brainer.

The nicest part is that all this is possible even without the expensive run-time polymorphism and RTTI, since the choose is done at compile-time.

Notes

[1] actually when targeting Linux, you usually target all Unix-like systems since they already have POSIX as a common abstraction

[2] The code is not real, it has been sweetened