Platform Abstraction with C++ Templates

Original Author: Michael Tedder

In a post a few months ago here on #AltDevBlogADay, Aras Pranckevičius discussed three approaches to eliminating the virtual function call overhead when using different class implementations on different platforms.

One approach that wasn’t introduced in that post was one which utilizes templates.  Although there was some discussion in the comments about making use of templates, some questions remained unanswered and no code was given, so I figured I’d post about how I use templates in my engine and hopefully fill in any missing information.

I’ll also show how to keep the code within a source file, avoiding the need to expose the implementations from a header — a common complaint with templates.  To keep things simple, I’ll just be showing how to implement a multiplatform debug print() function which sends a string to the debugger or console.

#define Your Platforms

The first step is to give each of the platforms you support a unique ID, adding each as a #define to a globally-#included header file.  If you’re already doing multiplatform development, you most likely already have such a list.  For example, something like the following will suffice:

1
 
  2
 
  3
 
  4
 
  5
 
  
#define PLATFORM_WINDOWS	1
 
  #define PLATFORM_LINUX		2
 
  #define PLATFORM_MACOS		3
 
  #define PLATFORM_ANDROID	4
 
  #define PLATFORM_IOS		5

Next, you’ll need to do whatever is necessary to detect the platform you are compiling for and #define it to one of the above values:

1
 
  2
 
  3
 
  4
 
  5
 
  6
 
  7
 
  8
 
  9
 
  10
 
  11
 
  12
 
  13
 
  14
 
  
#if defined(_WIN32)
 
   #define PLATFORM_ID		PLATFORM_WINDOWS
 
  #elif defined(__ANDROID__)		// must come before __linux__ as Android also #defines __linux__
 
   #define PLATFORM_ID		PLATFORM_ANDROID
 
  #elif defined(__linux__)
 
   #define PLATFORM_ID		PLATFORM_LINUX
 
  #elif defined(__MACH__)
 
   #include <TargetConditionals.h>
 
   #if (TARGET_OS_IPHONE == 1)
 
    #define PLATFORM_ID		PLATFORM_IOS
 
   #else
 
    #define PLATFORM_ID		PLATFORM_MACOS
 
   #endif
 
  #endif

How It Works

Instead of declaring a base interface class with virtual functions then deriving each platform with a different implementation, we declare a class with one template parameter — a platform ID — then specialize it to provide a different implementation for each platform.  The template class is then typedef‘d to expose the specialization for the platform ID being compiled to the application, allowing the implementation to be used without any virtual functions and also allow for inlining of functions as well.

One of the more interesting features of using a template-based approach is that if a specialization for a specific platform isn’t defined, then the initial template definition can be used as a ‘default’ or generic implementation.  This allows for:

  1. Platforms that do not require any specific implementation can use the generic implementation, cutting down on the amount of code necessary, and
  2. Easier porting to different platforms (as the generic implementation can be used until a specialization is provided), avoiding a mass of compilation errors when a new platform is added.

The default template definition and specialized declarations are placed in the header file.  We’ll put this code in a separate namespace to allow us to use the same class name for both the specializations and the interface exposed to the application:

1
 
  2
 
  3
 
  4
 
  5
 
  6
 
  7
 
  8
 
  9
 
  10
 
  11
 
  12
 
  13
 
  14
 
  15
 
  16
 
  17
 
  18
 
  19
 
  20
 
  21
 
  22
 
  23
 
  24
 
  25
 
  26
 
  27
 
  28
 
  29
 
  30
 
  31
 
  32
 
  33
 
  34
 
  35
 
  
#include <cstdio> 
 
   
 
  namespace Private
 
  {
 
  	// generic declaration (the base interface class)
 
   	template <int PlatformID>
 
  	class Debug
 
  	{
 
  	public:
 
  		static void print(const char *str);
 
  	};
 
   
 
  	// specialization for Windows platform (the derived class for Windows)
 
  	template<>
 
  	class Debug<PLATFORM_WINDOWS>
 
  	{
 
  	public:
 
  		static void print(const char *str);
 
  	};
 
   
 
  	// specialization for Android platform (the derived class for Android)
 
  	template<>
 
  	class Debug<PLATFORM_ANDROID>
 
  	{
 
  	public:
 
  		static void print(const char *str);
 
  	};
 
   
 
  	// generic platform (base interface class) implementation
 
  	template <int PlatformID>
 
  	void Debug<PlatformID>::print(const char *str)
 
  	{
 
  		::puts(str);
 
  	}
 
  }

In the code above, we declare two specializations: one for Windows and another for Android.  The generic implementation, which will be used on all other platforms (Linux, MacOS, and iOS) is also provided, and defined to simply chain to the C library’s puts() function.

Note that code for the generic implementation is required to be in the header.  This is so the compiler can supply a function for the template which does not have an explicit specialization.  For example, when compiling on Linux, the linker will look for a function named Private::Debug<2>::print(const char *).  If that function isn’t defined in any of the source files, and there isn’t any generic implementation, then the linker will give you a nice error message.

Next, we’ll add the typedef in the header to allow the application to utilize the proper implementation for the platform being compiled:

1
 
  
typedef Private::Debug<PLATFORM_ID>	Debug;

We also need to provide the specializations for both Windows and Android platforms.  These specializations can be placed in a source file, and defined by simply using our Debug class definition, since we typedef‘d it in our header above:

1
 
  2
 
  3
 
  4
 
  5
 
  6
 
  7
 
  8
 
  9
 
  10
 
  11
 
  12
 
  13
 
  14
 
  15
 
  16
 
  17
 
  18
 
  19
 
  20
 
  21
 
  22
 
  23
 
  24
 
  
#include "debug.h"
 
   
 
  #if (PLATFORM_ID == PLATFORM_WINDOWS)
 
   
 
  #include <windows.h>
 
   
 
  // implementation for Windows
 
  void Debug::print(const char *str)
 
  {
 
  	::OutputDebugStringA(str);
 
  	::OutputDebugStringA("n");
 
  }
 
   
 
  #elif (PLATFORM_ID == PLATFORM_ANDROID)
 
   
 
  #include <android/log.h>
 
   
 
  // implementation for Android
 
  void Debug::print(const char *str)
 
  {
 
  	::__android_log_print(ANDROID_LOG_INFO, "MyApp", str);
 
  }
 
   
 
  #endif

Finally, a simple main() function to show how it’s used:

1
 
  2
 
  3
 
  4
 
  5
 
  6
 
  7
 
  
#include "debug.h"
 
   
 
  int main()
 
  {
 
  	Debug::print("hello world!");
 
  	return 0;
 
  }

… and you now have a cross-platform debug print function.

But Wait, This Wasn’t About Graphics!

Indeed, the post Aras Pranckevičius made earlier discussed about abstracting a graphics device based on the platform.  The same logic can be applied here as well, but only to those platforms which have only one kind of device.  For example, a PS3 might only ever use a GCM device, and could be a likely candidate for using this type of abstraction.  For a PC however, it is possible to have both OpenGL and DirectX rendering support (or software rendering, anyone?), and dynamically switching between these interfaces is something that cannot be done at runtime with templates.

Some good candidates for using this template-based abstraction would be OS low-level support classes, such as: events, mutexes, fibers, threads, and clock/timer handling code — classes which need to be efficient and only have a one-to-one mapping with the platform.  A vector math/SIMD class could also be a good candidate as well, as the member functions can be inlined.  For higher-level classes that provide graphics and sound support, using an interface class with virtual methods is sufficient enough.

The point to be made is that there it is important to use the right tool for the job.  The template abstraction method presented here is not intended to solve all of your problems, it is just one method of many to assist in cross-platform development.