Monday, March 21, 2011

Implementation details

A program is composed of building blocks, or layers. Ideally, each layer should be independent and communicate in a way that changes in a deeper layer do not affect a higher layer, unless the public interface, the facade, is modified. This is the basis for encapsulation: do not expose what does not need to be exposed.

As an example, take a function get_page(const uri&) that queries a web server using HTTP and returns the content of the page as a string. Whether get_page() uses raw sockets, libcurl or Boost.Asio should not affect the calling code, since the facade is the same: give me a URI and I'll give you back a string.

There are many ways of isolating layers so that implementation details are hidden. Some allow only a relink, while others force a recompile, although without requiring modifications to the calling code. Some are easier on maintenance, but are more complex to implement.

As with most things in programming, there is always a compromise to be made between development time and maintenance time. Experience will make the extent of this compromise easier to define, based on the nature of the program, time constraints, skill and managerial issues. While ignoring encapsulation may be just fine for small, quick-and-dirty programs, it is not for larger codebases. Remember also that what started as a small program can easily get out of hands if its scope gets larger with time. I've been bitten more than once by the "bah, 'tis just a small debug thing, oh darn now it needs a boatload of features, oh crap it needs to be ported to Linux, oh god help me now it's got to run in production."

Never assume that your "small debug thing" will stay that way. Always program as well as your time constraints allow.

The following examples are taken from a real project that pretty much went through these "oh crap" steps. Although it needed some refactoring several times, using some kind of encapsulation from the start helped me reduce the time needed for modifications.

The final version of this program takes a list of (x, y) points from an external source (such as a CSV file) and displays them on an OpenGL context. It needs to support basic graphical operations such as zooming and panning, as well as data modifications such as rotating and translating the points. It has to run on both Windows and QNX.

The examples will concentrate on the OpenGL part, because it can demonstrate the different methods of encapsulation:
  • Windows supports OpenGL 1.1 out of the box, while QNX only supports OpenGL ES 1.0, a subset of OpenGL.
  • The creation of the OpenGL context and user interface elements is different on both platforms.
  • OpenGL ES is messy to use and benefits from some sort of wrapping.
Because not everybody may be acquainted with OpenGL or QNX, the examples will not require any special knowledge. This is not a tutorial on using OpenGL and therefore the code using it will not be complete.

The following is used throughout this article:
#include <vector>

// either of those is defined to 1 on the right platform
#define QNX 0
#define WIN 0

struct point
{
  int x, y;

  point()
    : x(0), y(0)
  {
  }
};

typedef std::vector<point> points;
 
points get_points()
{
  points ps;

  // loads the point from an external source

  return ps;
}
It also uses Boost.Foreach for simplicity:
#include <boost/foreach.hpp>
#define foreach BOOST_FOREACH

No encapsulation
The simplest in term of initial development time is to ignore encapsulation (or any other method of making the code "better".) Doing so will quickly give you a working program that will probably do exactly what you want. Let's make one.

The development started on Windows with OpenGL 1.1. It needs to create a window and an OpenGL context to draw on.
// opengl_context.h

class opengl_context
{
public:
  opengl_context(HWND h);
  ~opengl_context();
  
  void begin(glEnum type);
  void add(const points& ps);
  void end();
  
private:
  HDC gl_;
};
// opengl_context.cpp

#include "opengl_context.h"

opengl_context::opengl_context(HWND h)
  : gl_(0)
{
  gl_ = // create the context using 'h'
}

opengl_context::~opengl_context()
{
  // destroy the context
  ReleaseDC(gl_);
}

void opengl_context::begin(glEnum type)
{
  // this tells OpenGL that the next calls will affect
  // the given context
  wglMakeCurrent(gl_, 0);

  // clear the screen
  glClearColor(0.0f, 0.0f, 0.0f, 0.0f);
  glClear(GL_COLOR_BUFFER_BIT);

  glBegin(type);
}

void opengl_context::add(const points& ps)
{
  foreach(const point& p, ps)
    glVertex2i(p.x, p.y);
}

void opengl_context::end()
{
  glEnd();
}
// main.cpp

#include "opengl_context.h"

HWND create_window();

int main()
{
  HWND h = create_window();

  opengl_context gl(h);
  points ps = get_points();

  gl.begin(GL_POINTS);
  gl.add(ps);
  gl.end();
  
  DestroyWindow(h);
}
Let's start with opengl_context. The member functions begin(), add() and end() are thin wrappers around glBegin(), glVertex2i() and glEnd(). This tells OpenGL that points are coming, adds them and then signals that all points are in and can be drawn on screen. begin() also selects the context and clears the screen.

The constructor somehow creates a context that is destroyed in the destructor.

Although this might seem simple enough, it's also a mess from the point of view of encapsulation. It exposes several things:
  • It exposes the Windows-specific type HDC to users of the class.
  • The interface assumes that begin(), add() and end() corresponds to an OpenGL idiom that's available on the platform.
  • It mixes the implementation-specific wglMakeCurrent() (part of the Windows OpenGL implementation), the OpenGL 1.1 calls (glBegin(), glEnd() and glVertex2i()) and functions available under both Windows and QNX (glClearColor() and glClear()).
Let's try porting this to QNX right away and see what happens.

First, a window handle is not a HWND anymore, it's a PtWidget_t*:
#include "opengl_context.h"

PtWidget_t* create_window();

int main()
{
  PtWidget_t* h = create_window();

  opengl_context gl(h);
  points ps = get_points();

  gl.begin(GL_POINTS);
  gl.add(ps);
  gl.end();
  
  PtDestroyWidget(h);
}
The entry point of a program is often going to be implementation-dependent because it usually requires setting up some sort of user interface. Unless this program is console-based program or uses a portable widget library, the entry point will usually be highly platform specific. Therefore, the use of the PtWidget_t* here is okay.

However, you'll run into trouble with the implementation of gl.begin() and gl.end() because the corresponding OpenGL function glBegin() and glEnd() don't exist in QNX. This is because OpenGL ES uses a completely different way of setting up the points to display, which is not easily represented by our begin(), add() and end() idiom.

The class definition of opengl_context also doesn't compile because it mentions HWND and HDC. Its implementation is also broken because of two reasons:
  1. it uses Windows-specific calls (such as ReleaseDC()), and
  2. it uses OpenGL 1.1 calls, while QNX only supports OpenGL ES 1.0
Clearly, the opengl_context class needs a major overhaul, which brings us to the different types of encapsulation that could be used.

Types of encapsulation
There is more than one way of modifying opengl_context to be portable. Obviously, the simple scheme used so far is very hard to port: there are platform-specific types in the class definition, platform-specific calls in the member functions and assumptions from the interface on how to interact with OpenGL.

There are several ways to hide the implementation details:
  • use conditional compilation so that only the right function calls and types are compiled in.
  • move all the platform specific calls to a different class and keep a pointer to it, using inheritance to choose the right implementation.
  • move all the platform specific calls to somewhere outside the class, use conditional compilation to choose the right implementation and let the linker do its work.
  • same as the previous point, but use policy and traits classes to choose the right implementation; this would require opengl_context to be templated.
Finally, a problem that cannot easily be fixed with idioms is having a portable API. This is a problem that is often ignored. An API needs to be abstract enough so it can be adapted to different backends. If not, it might be awkward to use, or even impossible to retrofit with the different implementation.

Let's start with the simplest.

Conditional compilation
As with everything, conditional compilation is simple and quick, but leads to hate and suffering for a variety of reasons.

Let's modify the class definition so it is portable under both Windows and QNX:
class opengl_context
{
public:
#if WIN
  opengl_context(HWND h);
#elif QNX
  opengl_context(PtWidget_t* h);
#endif

  ~opengl_context();
  
  void begin(glEnum type);
  void add(const points& ps);
  void end();
  
private:
#if WIN
  HDC gl_;
#elif QNX
  EGLContext gl_;
#endif
};
I think it is quite obvious that this scheme does not scale well, but let's look at the implementation of members functions:
// opengl_context.cpp

#include "opengl_context.h"

#if WIN
opengl_context::opengl_context(HWND h)
#elif QNX
opengl_context::opengl_context(PtWidget_t* h)
#endif
  : gl_(0)
{
  gl_ = // create the context using 'h'
}

opengl_context::~opengl_context()
{
  // destroy the context

#if WIN
  ReleaseDC(gl_);
#elif QNX
  eglDestroyContext(.., gl_);
#endif
}

void opengl_context::begin(glEnum type)
{
  // this tells OpenGL that the next calls will affect
  // the given context

#if WIN
  wglMakeCurrent(gl_, 0);
#elif QNX
  eglMakeCurrent(.., gl_);
#endif

  // clear the screen
  glClearColor(0.0f, 0.0f, 0.0f, 0.0f);
  glClear(GL_COLOR_BUFFER_BIT);

  glBegin(type);
}

void opengl_context::add(const points& ps)
{
  foreach(const point& p, ps)
    glVertex2i(p.x, p.y);
}

void opengl_context::end()
{
  glEnd();
}
There are many problems with this:
  • What if we need to support a third platform? Making sure every case is covered is a nightmare, unless you add an #else with some kind of #error. But this needs discipline: what if you forget to change one instance which happens to compile but not do what is expected?
  • This makes it harder to see what's happening because although the different cases may be equivalent (both are constructors taking a handle, for example), they are duplicated. It breaks the normal flow of operations in such a way that understanding the code is much harder.
  • It creates many variations of the same program. This is the case with any sort of conditional compilation: depending on a macro, the compiler will see different things. This is a nightmare to maintain and test correctly.
  • I see #ifdef being used a lot more than #if in these cases. This adds another danger, since #ifdef obviously cannot distinguish between a typo and a name that was deliberately not defined.
This is clearly not the solution. However, programming means compromises. What if, throughout hundreds of lines of code, only one had to change between platforms? The following schemes might be overkill. What's important to remember is that:
  1. Programs evolve. Try to be prepared for as many changes as possible, especially in areas like portability. If there's one line that needs to be ifdef'ed, there's probably going to be more eventually.
  2. Discipline is hard. If parts of your code requires discipline to maintain (such as adding a new #elif), it is brittle.
  3. Readability is paramount. Code needs to be simple and easy to read, not only for others' sake, but for yours. It is only a matter of days, not weeks nor months, before you starting looking at your code with fresh eyes.
Note that all the given schemes have to use some sort of conditional compilation if that particular code just wouldn't compile. This is fine, as long as what's being compiled out is the whole code segment itself, such as an entire class. The problems begin when the conditional compilation is spread throughout the code.

Implementation class with inheritance
A common way of hiding the implementation of a class is to move all the platform-specific code to a separate class and keep an opaque pointer to it. This is often called the "pimpl" idiom, from the name pImpl that is often given to the pointer. An opaque pointer is simply a pointer to an incomplete type, that is, a type that has only been declared, not defined.

The advantage is that everything that is platform-specific does not leak into the calling code. It also makes it possible to completely separate the implementations from each other, which makes the code clearer, while leaving the common code together.

Designing a good implementation class is not easy: it needs to be flexible enough so it can be used in different ways. A member function like impl::update() might look alright, until you realize it is too general because you need to execute only a part of it. You then need to break it down in smaller pieces. Note that this is not a bad thing in itself, since it prevents you from doing some copy/pasting that you otherwise might be tempted to do.

The "pimpl" idiom is usually implemented with a base class, let's call it opengl_context_impl, from which implementation-specific classes derive, let's call them opengl_context_win and opengl_context_qnx. One of those is created on the heap and a pointer to the base class is kept.

There are two main problems with this:
  1. It uses the heap and virtual functions. Not all applications can use the heap freely (such as in embedded computers) and forwarding to a virtual function through a pointer to a base class is usually slower than calling a member function directly.
  2. It does not solve the problem of passing implementation-defined values through the facade to the implementation, such as the HWND or PtWidget_t* in the constructor.
There are several workarounds for 2):
  1. Pass a void* or some type such as an unsigned int that can be forwarded to the implementation and converted to the right type. This is dangerous because the size of a void* or unsigned int might not be the same as a HWND or a PtWidget_t*. You can also end up with invalid bit patterns because of the reinterpret_cast involved, which is undefined behavior. It also looses type information, forcing the implementation to assume that the value given is alright.
  2. Have the calling code create the implementation class and give it to the facade in its constructor. This is probably the simplest way. The implementation still leaks to the calling code, but it is hopefully in a place where there is already platform-specific stuff anyways (such as in an entry point where the inherently platform-specific user interface is created.) However, unless the calling code remembers that pointer, it looses access to the implementation. It might be necessary, for example, to retrieve the window handle (HWND or PtWidget_t*) so it can be passed to another function.
  3. Either make the facade templated on the types needed or use traits classes. This is a more complex approach that is detailed below.
Let's try separating opengl_context into the two platform-specific classe. First, the implementation classes:
class opengl_context_impl
{
public:
  virtual ~opengl_context_impl();
  virtual void make_current() = 0;
};
#if WIN
class opengl_context_win : public opengl_context_impl
{
public:
  opengl_context_win(HWND h)
    : gl_(0)
  {
    gl_ = // create the context using 'h'
  }
  
  virtual ~opengl_context_win()
  {
    // destroy the context
    ReleaseDC(gl_);
  }
  
  virtual void make_current()
  {
    wglMakeCurrent(gl_, 0);
  }
  
private
  HDC gl_;
};
#endif
#if QNX
class opengl_context_qnx : public opengl_context_impl
{
public:
  opengl_context_qnx(PtWidget_t* h)
    : gl_(0)
  {
    gl_ = // create the context using 'h'
  }
  
  virtual ~opengl_context_qnx()
  {
    // destroy the context
    eglDestroyContext(.., gl_);
  }
  
  virtual void make_current()
  {
    eglMakeCurrent(.., gl_);
  }
  
private
  EGLContext gl_;
};
#endif
This neatly packages platform-specific stuff in different classes. They might also be in different files, which makes it even easier to distinguish and modify. Note that these two classes will probably have to be conditionally compiled if they won't compile in a particular environment.

Modifying opengl_context to use the implementation is easy:
// opengl_context.h

class opengl_context_impl;

class opengl_context
{
public:
  opengl_context(std::auto_ptr<opengl_context_impl> impl);
  
  void begin(glEnum type);
  void add(const points& ps);
  void end();
  
private:
  std::auto_ptr<opengl_context_impl> impl_;
};
// opengl_context.cpp

#include "opengl_context.h"

opengl_context::opengl_context(std::auto_ptr<opengl_context_impl> impl)
  : impl_(impl)
{
}

void opengl_context::begin(glEnum type)
{
  // this tells OpenGL that the next calls will affect
  // the given context
  impl_->make_current();

  // clear the screen
  glClearColor(0.0f, 0.0f, 0.0f, 0.0f);
  glClear(GL_COLOR_BUFFER_BIT);

  glBegin(type);
}

void opengl_context::add(const points& ps)
{
  foreach(const point& p, ps)
    glVertex2i(p.x, p.y);
}

void opengl_context::end()
{
  glEnd();
}
Finally, main(). It still uses macros to separate platform-specific code from the now almost portable opengl_context:
#include "opengl_context.h"

#if WIN
  typedef HWND handle_type;
  typedef opengl_context_win impl_type;
#elif QNX
  typedef PtWidget_t* handle_type;
  typedef opengl_context_qnx impl_type;
#endif

handle_type create_window();

int main()
{
  handle_type h = create_window();   
  opengl_context gl(
    std::auto_ptr<opengl_context_impl>(new impl_type(h)));
  
  points ps = get_points();

  gl.begin(GL_POINTS);
  gl.add(ps);
  gl.end();
  
#if WIN
  DestroyWindow(h);
#elif QNX
  PtDestroyWidget(h);
#endif
}
One issue we haven't tackled yet is the glBegin(), glVertex2i() and glEnd() problem I mentioned briefly before. These functions are unavailable in QNX because only a subset of OpenGL is supported. Fortunately, this subset is available on both Windows and QNX. Therefore, these calls need to be replaced by the subset (namely, vertex buffers). This will also affect the API of opengl_context.

Now, this problem is not specific to my example, nor to OpenGL or QNX. This is only a way of showing a common problem: because portability was not part of the original specifications, no research has been done on the differences between the platforms. In this example, I would have found that Windows supports OpenGL 1.1 while QNX only supports OpenGL ES 1.0. Other examples could include different socket implementations, user interfaces, filesystems, hardware access or threading facilities.

What's important here is that the differences between platforms might affect the API. Design your API to match how a specific implementation works too closely, that is, create an API that is not abstracted enough, and you'll run into trouble when you try to port it.

So let's modify opengl_context and see what happens. In this case, the API needs to be broken down in smaller pieces. Other cases would see the API get more complex or even radically different.
// opengl_context.h

class opengl_context_impl;

class opengl_context
{
public:
  opengl_context(std::auto_ptr<opengl_context_impl> impl);
  
  void make_current();
  void clear();
  void add(const points& ps);
  
private:
  std::auto_ptr<opengl_context_impl> impl_;
};
// opengl_context.cpp

#include "opengl_context.h"

opengl_context::opengl_context(std::auto_ptr<opengl_context_impl> impl)
  : impl_(impl)
{
}

void opengl_context::make_current()
{
  // this tells OpenGL that the next calls will affect
  // the given context
  impl_->make_current();
}

void opengl_context::clear()
{
  glClearColor(0.0f, 0.0f, 0.0f, 0.0f);
  glClear(GL_COLOR_BUFFER_BIT);
}

void opengl_context::add(const points& ps)
{
  glEnableClientState(GL_VERTEX_ARRAY);
  glVertexPointer(2, GL_INT, 0, &ps[0]);
  glDrawArrays(GL_POINTS, 0, static_cast(ps_.size()));
  glDisableClientState(GL_VERTEX_ARRAY);
}
What was in begin() before is now separated into make_current() and clear(). The biggest change is in add(), which instead of adding the individual points in a loop now gives a pointer to the array. The calling code then becomes:
int main()
{
  handle_type h = create_window();   
  opengl_context gl(
    std::auto_ptr<opengl_context_impl>(new impl_type(h)));
  
  points ps = get_points();

  gl.make_current();
  gl.clear();
  gl.add(ps);
  
#if WIN
  DestroyWindow(h);
#elif QNX
  PtDestroyWidget(h);
#endif
}
Although these changes were small, it shows that even if you are using some idiom to separate the implementation details, the calling code may still need to be modified if the public API is too close to a specific implementation.

A problem with this scheme is that accessing the implementation is now impossible, unless a opengl_context::get_impl() is added and its return value cast to the appropriate derived class. The reasons for accessing the implementation are varied, but one that comes to mind is a Windows-only function that could set the keyboard focus on the window that contains a specific context:
void set_focus(opengl_context& c)
{
  ::SetFocus(c.??);
}
Getting the window handle from the context is complicated because the opengl_context object has no easy way of making the right type available.
  1. You could try adding a get_impl() member function that returns a opengl_context_impl, but even adding a virtual get_handle() wouldn't help because you can't return the right type.
  2. You could try returning a void* or another generic type and cast it to the right handle type, but that has many problems as mentioned before (such as mismatched sizes, bit patterns, etc.)
  3. You also could cast what get_impl() returns to the right implementation type (such as opengl_context_win) and add a non-virtual get_handle() that returns the right handle type (such as HWND) to it.
  4. Finally, you could make set_focus() a virtual function in the base class that could be implemented correctly by the derived classes, but this opens a can of worms: how many of these functions do you need to add to the base class? If a platform has no notion of keyboard focus, does it make sense to add set_focus() to the base class? What about a move_window() or a capture_mouse()?
So there are two main problems with a inheritance-based implementation class: efficiency (virtual functions are called through a pointer) and the loss of type information. The best tools to get that information back are static typing and templates. However, the kind of separation offered by inheritance is still very useful.

It also allows changing the implementation at runtime in certain cases. For example, a database class could be able to switch at runtime from a database_mysql to a database_sqlite depending on the file that is currently loaded.

Implementation class with static types
In cases such as a platform-specific implementations that cannot be switched at runtime (like our opengl_context class), static types can be more efficient and flexible than inheritance-based types used through a pointer to the base class.

Basically, you remove the base class opengl_context_impl and have opengl_context either inherit from the right implementation class or keep it as a member object (not a pointer). By naming the two implementations classes identically, you let the compiler decide which implementation class to include based on macros and the linker will put this together.

Let's get back to the implementation:
#if WIN
class opengl_context_impl
{
public:
  typedef HWND handle_type;

  opengl_context_impl(HWND h)
    : gl_(0)
  {
    gl_ = // create the context using 'h'
  }
  
  ~opengl_context_impl()
  {
    // destroy the context
    ReleaseDC(gl_);
  }
  
  void make_current()
  {
    wglMakeCurrent(gl_, 0);
  }
  
private:
  HDC gl_;
};
#endif
#if QNX
class opengl_context_impl
{
public:
  typedef PtWidget_t* handle_type;

  opengl_context_impl(PtWidget_t* h)
    : gl_(0)
  {
    gl_ = // create the context using 'h'
  }
  
  ~opengl_context_impl()
  {
    // destroy the context
    eglDestroyContext(.., gl_);
  }
  
  void make_current()
  {
    eglMakeCurrent(.., gl_);
  }
  
private:
  EGLContext gl_;
};
#endif
The main difference here is that both classes are named opengl_context_impl and contain no virtual functions. They are ifdef'ed so that the right one is compiled in. Let's look at how opengl_context interacts with them:
class opengl_context
{
public:
  opengl_context(opengl_context_impl::handle_type h);
  
  void make_current();
  void clear();
  void add(const points& ps);
  
private:
  opengl_context_impl impl_;
};
opengl_context::opengl_context(opengl_context_impl::handle_type h)
  : impl_(h)
{
}

void opengl_context::make_current()
{
  // this tells OpenGL that the next calls will affect
  // the given context
  impl_.make_current();
}

// clear() and add() are identical
int main()
{
  opengl_context_impl::handle_type h = create_window();   
  opengl_context gl(h);
  points ps = get_points();

  gl.make_current();
  gl.clear();
  gl.add(ps);
  
#if WIN
  DestroyWindow(h);
#elif QNX
  PtDestroyWidget(h);
#endif
}
This scheme looks a lot like the inheritance-based one, but with one major difference: the implementation is not a pointer anymore, it's an object. opengl_context could also inherit from the implementation class, this would make no difference except from the visibility of the implementation functions.

Both ways are arguable, but I prefer reducing the visibility as much as I can. If I found out that pretty much all the member functions from opengl_context_impl need to be accessed from the outside, I'd publicly inherit from it. If not, I'd aggregate an object, add the functions needed to opengl_context and forward them. This could be offset by making the member functions protected, but this would force users to always inherit from it.

The main problem is that the implementation now needs to be constructed from opengl_context. Currently, it uses the handle_type typedef and therefore does not need any conditional compilation. However, this assumes that the constructor of both implementation classes can be called identically (like it is the case right now, with only a window handle). If not, then the constructor would either need to be ifdef'ed or, better, an initialization structure could be defined by the implementation and filled from the calling code:
#if WIN
struct opengl_context_init
{
  HWND h;
};

class opengl_context_impl
{
public:
  opengl_context_impl(const opengl_context_init& init);

  // as before
};
#endif
#if QNX
struct opengl_context_init
{
  PtWidget_t* h;
  gf_dev_t gfdev;
  PdOffscreenContext_t* offscreen;
};

class opengl_context_impl
{
public:
  opengl_context_impl(const opengl_context_init& init);
  // as before
};
#endif
class opengl_context
{
public:
  opengl_context(const opengl_context_init& init);
  
  // as before
};
opengl_context::opengl_context(const opengl_context_init& init)
  : impl_(init)
{
}

void opengl_context::make_current()
{
  // as before
  // ...
int main()
{
  opengl_context_init init;

#if WIN
  init.h = create_window();
#elif QNX
  init.h = create_window();
  init.gfdev = gf_dev_attach(..);
  init.offscreen = PdCreateOffscreenContext(..);
#endif
  
  opengl_context gl(init);

  // as before
}
Now, because this scheme uses static typing, types are still available. Therefore, we could add this handle() member function correctly:
class opengl_context
{
public:
  opengl_context(const opengl_context_init& init);

  opengl_context_impl::handle_type handle();  

  // as before
};
void set_focus(opengl_context& c)
{
  ::SetFocus(c.handle());
}
I find this scheme to have the best of both world:
  1. The implementations are still cleanly separated from the common code.
  2. The platform-specific information needed for the initialization is taken out of opengl_context and moved to the calling code, which might already be platform-specific anyways (such as in main()).
  3. There is no efficiency penalty: member functions are called through an object using its static type. No virtual functions or pointers are involved.
  4. Type information is retained because no inheritance is used.
However, as mentioned previously, this can only be used when the implementation cannot change at runtime.

Implementation class with templates
The previous examples with static typing work well in most cases. However, this assumes that all implementations will support the same operations and that only one implementation can be selected at compile-time.

Let's add a member function to opengl_context that can return some information, such as the current model view matrix. The details are unimportant: this is just an array of 16 floats that is returned from a call to glGetFloatv().

However, this function is unavailable on QNX because it is not part of OpenGL ES 1.0. In the particular program I am taking these examples from, the user interfaces in QNX and Windows are substantially different, such that the QNX port never needs to call it.

Therefore:
std::vector<float> opengl_context::model_view()
{
  float matrix[16];
  glGetFloatv(GL_MODELVIEW_MATRIX, matrix);

  return std::vector<float>(matrix, matrix+16);
}
just wouldn't compile. One solution would be to ifdef it, but this has lots of issues, like explained previously. You could have opengl_context::model_view() forward to the platform-specific opengl_context_impl::model_view(), but that would require the QNX version to also implement it, perhaps as a function that returns an empty vector or raises an assert. However, always try to rely on the compiler instead of on runtime errors. Runtime errors require testing, whereas compile-time errors don't.

You could implement model_view() in the Windows version of opengl_context_impl, but not in the QNX version and inherit from the implementation class instead of aggregating an object. This way, calling model_view() from Windows would work, while calling it from QNX would give a compilation error. This, however, requires inheritance. Although this is not necessarily a bad thing, it can be implemented in a way that both inheritance and aggregation are supported.

By making opengl_context templated on the implementation type, you can add member functions which wouldn't necessarily compile if called, but won't cause problems if left alone.

Let's start with the implementation classes:
#if WIN
class opengl_context_win
{
public:
  typedef HWND handle_type;

  struct init
  {
    handle_type h;
  };
  
  
  opengl_context_win(const init& i)
    : gl_(0)
  {
    gl_ = // create the context using 'i'
  }
  
  ~opengl_context_win()
  {
    // destroy the context
    ReleaseDC(gl_);
  }
  
  void make_current()
  {
    wglMakeCurrent(gl_, 0);
  }
  
  std::vector<float> model_view()
  {
    float matrix[16];
    glGetFloatv(GL_MODELVIEW_MATRIX, matrix);

    return std::vector<float>(matrix, matrix+16);
  }

private:
  HDC gl_;
};
#endif
#if QNX
class opengl_context_qnx
{
public:
  typedef PtWidget_t* handle_type;

  struct init
  {
    handle_type h;
    gf_dev_t gfdev;
    PdOffscreenContext_t* offscreen;
  };

  opengl_context_qnx(const init& i)
    : gl_(0)
  {
    gl_ = // create the context using 'i'
  }
  
  ~opengl_context_qnx()
  {
    // destroy the context
    eglDestroyContext(.., gl_);
  }
  
  void make_current()
  {
    eglMakeCurrent(.., gl_);
  }
  
private:
  EGLContext gl_;
};

#endif
The classes now have distinct names and I've moved the initialization structure inside. The Windows implementation has a model_view(), but the QNX one does not. Let's look at opengl_context:
template <class Impl>
class basic_opengl_context
{
public:
  basic_opengl_context(const typename Impl::init& init)
    : impl_(init)
  {
  }
  
  void make_current()
  {
    impl_.make_current();
  }
  
  void clear()
  {
    glClearColor(0.0f, 0.0f, 0.0f, 0.0f);
    glClear(GL_COLOR_BUFFER_BIT);
  }
  
  void add(const points& ps)
  {
    glEnableClientState(GL_VERTEX_ARRAY);
    glVertexPointer(2, GL_INT, 0, &ps[0]);
    glDrawArrays(GL_POINTS, 0, static_cast(ps_.size()));
    glDisableClientState(GL_VERTEX_ARRAY);
  }
  
  std::vector<float> model_view()
  {
    return impl_.model_view();
  }

private:
  Impl impl_;
};
I've renamed opengl_context to basic_opengl_context. Since it is now a template, using a typedef for opengl_context with the right implementation is easier. Now, this:
#if WIN
typedef basic_opengl_context<opengl_context_win> opengl_context;
#elif QNX
typedef basic_opengl_context<opengl_context_qnx> opengl_context;
#endif

void f(opengl_context& c)
{
  c.model_view();
}
would compile fine on Windows (where model_view() is defined and glGetFloatv() available), but would not on QNX (because model_view() is not defined).

One of the advantages of using a template is that it is easy to plug in different implementations at compile-time. Relying on conditionally compiled classes with the same name works when only one implementation may be selected (which is the case for our opengl_context). For example, a basic_database<> could be used with either a database_sqlite or a database_mysql.

The main downside to making opengl_context a template is that on most compilers, the member function definitions must be visible along with the class definition (unless you're in luck and have access to a compiler that supports export).

Wrapping up
Using conditional compilation is quick and easy, but is a nightmare in the long run. It spreads implementation-specific code throughout the program, with no easy way of having a general view of what runs in which case. It is the equivalent of magic numbers. It makes it harder to know exactly what the compiler is seeing with a particular set of macros. It also clutters the code with different versions of the same code, making it harder to follow the execution path.

Hiding the implementation behind a pointer works great when the particular implementation may change at runtime, but it has associated costs because it needs to call functions through a pointer and a virtual table. It also looses any type information present in the implementation class (such as the type of a window handle), forcing the user to get a pointer to the implementation and cast it to the appropriate type.

Using conditional compilation to select between two different implementations only works if the specific implementation cannot change at runtime. The advantage in this case is that type information is retained and function calls go through an object using its static type.

Both the "pimpl" and static type idioms have the advantage of consolidating all the platform specific code in the same place, making it easy to see what code is fed to the compiler. It is also easier to maintain. By looking at the calling code, it is possible to quickly understand what part is generic and what part is implementation-specific.

Making a generic class templated on the implementation class allows for adding member functions that would not necessarily compile otherwise. If not called on an implementation where these are unavailable, no error will be generated. It also makes it possible to easily switch between implementations at compile-time, without relying on macros (unless an implementation cannot compile on a particular platform or setup.) However, the cost is that on most compilers, the member function definitions have to be visible alongside the class definition.

Finally, the API of a particular class needs to be abstract enough so that it can be used naturally on different platforms where the backend might be substantially different.

As usual, YMMV.

[edit 29-sept-2011: layout]

No comments: