Channels ▼
RSS

Imagine


Conversations: Imagine

Portability without #idefs? It's (sort of) easy if you really try.

    Imagine there's no ifdefs
    It's easy if you try
    No badly ported source
    Making programmers cry
    Imagine you are eas'ly
    Reading all the code...[1]

"Well, that was easier than I thought," I sighed to myself as I stretched out the kinks in my neck. I had just finished porting some of our application's code to a different platform and was giving it one last once-over before checking it in.

#ifdef _WIN32
#include <windows.h>
#else
#include <sys/select.h>
#endif

#ifndef _WIN32
int mystrncmp( const char * string1,
    const char * string2,
    size_t max
 )
{
  ... custom implementation of
  ... case-insensitive strncmp
}
#endif

int someClass::f(
#ifdef _WIN32
  HANDLE waitHandle,
#else
  int    waitDescriptor,
#endif
  const char * name )
{
#ifdef _WIN32
  bool match = _strnicmp( name,
        knownValue_, max_ ) == 0;
#else
  bool match = mystrncmp( name,
        knownValue_, max_ ) == 0;
#endif
  if ( match )
  {
    int waitResult;
#ifdef _WIN32
    waitResult = WaitForSingleObject(
        waitHandle,
        timeout_ );
    if ( waitResult == WAIT_OBJECT_0 )
    {
      waitResult -= WAIT_OBJECT_0;
    }
#else
    struct timeval timeout;
    fd_set fds;
    FD_SET( 0, &fds );
    timeout.tv_sec = timeout_ / 1000;
    timeout.tv_usec = (timeout_ % 1000)
      * 1000;
    waitResult = select( 1, &fds,
                NULL, NULL,
                &timeout );
#endif
    if ( waitResult != 0 )
    {
      ... error handling and
      ... exit function
    }
    ... whatever
  }
}
I reached for the keyboard to check the code in. Snap! I jumped as the Guru announced her presence in her customary way: snapping closed a large book inches away from my ear. (At least, that's what it felt like -- Wendy always insisted the Guru was always several feet away from me.)

"Call you that portable code, do you my apprentice?" the Guru asked in a firm voice. "Unreadable, it is, and brittle."

"Well, I agree it's not the prettiest code, but I don't think it's that unreadable," I argued.

"Not yet," the Guru replied, "but it is only a matter of time. My child, remember that we write software that runs on several different platforms. When the code is ported to a third platform, you will need another layer of conditional code. The code will quickly degrade into an unreadable morass of conditional statements, until it resembles the worst open-source project." She shuddered melodramatically [2]. "Conditionally compiled code, especially in the name of portability, is almost always a mistake. Indeed, many pious scribes are able to write portable code without resorting to this hideousness -- although these same scribes are quick to point out that their own experience does not always cover all possible situations [3]."

"But it's supposed to be portable code," I protested. "How am I supposed to write it portably if I can't use any conditional compilation?"

"My apprentice, have you forgotten that you have more tools than conditional compilation at your disposal? First let us remove the conditional directives surrounding #include directives." The Guru pointed at the top of my code:

#ifdef _WIN32
#include <windows.h>
#else
#include <sys/select.h>
#endif
"These directives can be replaced by a single #include statement. In this case, the include statements are there to pick up declarations required for waiting," she picked up a whiteboard marker and began writing in her fine script:

#include "platform_wait.h"
"The exact contents of that file will be platform-specific. For example, the first file will contain:"

// file: windows/platform_wait.h
#include <windows.h>
"and the second file," she continued, "will contain:"

// file: others/platform_wait.h
#include <sys/select.h>
"Most, if not all, compilers allow you control over which directories to search for included files. Each platform's make file will take advantage of this option, to specify exactly which file gets included by the directive. The make file for platform A will include the command-line option '/I include/windows', and the make file for platform B will include the option '/I include/others'."

"Now, my child, let us examine another part of the code," the Guru highlighted certain sections of my code:

#ifndef _WIN32
int mystrncmp( const char * string1,
    const char * string2,
    size_t max
 )
{
  ... custom implementation of
  ... case-insensitive strncmp
}
#endif

{
#ifdef _WIN32
  bool match = _strnicmp( name,
        knownValue_, max_ ) == 0;
#else
  bool match = mystrncmp( name,
        knownValue_, max_ ) == 0;
#endif
"This code demonstrates one of the two classic cases where conditional compilation is simply tossed in. You have one platform that provides a case-insensitive strncmp function, and another that does not. Thus, on some platforms you must emulate the behavior provided by other platforms.

"Rather than use two different code paths, prefer to use a single code path. In this situation, always use the emulation. The resulting code is much simpler, and easier to read and follow. The prophets Spencer and Collyer point out that five levels of conditional compilation -- a depth not too unlikely in a widely-ported application -- results in 32 potential code paths, each of which must be tested [4]. In any case, you must provide the custom function, so there is no benefit to sometimes using the built-in functionality, and sometimes using the custom functionality.

"And finally, here," she indicated the remaining code:

int someClass::f(
#ifdef _WIN32
  HANDLE waitHandle,
#else
  int    waitDescriptor,
#endif
  const char * name )
{
  if ( match )
  {
    int waitResult;
#ifdef _WIN32
    waitResult = WaitForSingleObject(
        waitHandle,
        timeout_ );
    if ( waitResult == WAIT_OBJECT_0 )
    {
      waitResult = 0;
    }
#else
    struct timeval timeout;
    fd_set fds;
    FD_SET( 0, &fds );
    timeout.tv_sec = timeout_ / 1000;
    timeout.tv_usec = (timeout_ % 1000) * 1000;
    waitResult = select( 1, &fds,
                NULL, NULL,
                &timeout );
#endif
    if ( waitResult != 0 )
    {
      ... error handling and
      ... exit function
    }
"your platforms provide substantially the same functionality, but with different implementations. Here, you can provide an abstraction of the functionality and extract the platform-specific code to individual source files, in a manner similar to the header files above. Spencer and Collyer refer to this as providing an interface. First, provide the interface:"

// file: common_wait.h
#include "platform_wait.h"
int wait( WaitObject, int timeout );

// file: windows/platform_wait.h
#include <windows.h>
typedef HANDLE WaitObject;

// file: others/platform_wait.h
#include <sys/select.h>
typedef int WaitObject;
"Now, we create small source files to implement the waiting:"

// file: windows/platform_wait.cpp
int wait
( WaitObject wo, timeout to )
{
  ... Windows implementation goes here
}

// file: others/platform_wait.cpp
int wait
( WaitObject wo, timeout to )
{
  ... other implementations go here
}
"Now, finally, your main line code is clean and simple:"

#include "common_wait.h"

int mystrncmp( const char * string1,
    const char * string2,
    size_t max
 )
{
  ... custom implementation of
  ... case-insensitive strncmp
}

int someClass::f(
  WaitObject waitHandle,
  const char * name )
{
  if (mystrncmp( name,
        knownValue_, max_ ) == 0 )
  {
    int waitResult =
      wait( waitHandle, timeout_ );

    if ( waitResult != 0 )
    {
      ... error handling and
      ... exit function
    }
    ... whatever
  }
}
"That's a lot of work, and a lot of files," I sighed.

"Portability is work, my child. Spencer and Collyer preach that writing readable, maintainable portable code is a fundamental design issue. Therefore great care and thought should go into writing portable code."

"Wait a second," I interjected. "What if you have to do some processing on one platform, but not on another? For example, I was looking at the source code for an open-source project, and on some platforms the program tries to lock the process into physical memory, because the application is time-sensitive. Not all platforms have that capability, though, so the code is surrounded by an #ifdef block."

"Ah, my child," the Guru said, "that is but a variation on a theme. Or, more precisely, it is a combination of the two situations. The first step is to create an interface:"

// file: process_lock.h
int lock_process();

// file: main application file
#include "process_lock.h"
void initialize()
{
  if ( lock_process() < 0 )
    ... error handling
  ... other init
}
"Now, we implement this function in platform-specific files:"

// file: platform_a/lock.cpp
int lock_process()
{
  ... perform steps specific
  ... to platform A
}

// file: platform_b/lock.cpp
int lock_process()
{
  ... perform steps specific
  ... to platform B
}

// file: platform_c/lock.cpp
int lock_process()
{
  // this platform doesn't
  // support locking
  return 0;
}
I sat silent for a moment, letting all this percolate through my brain. "So, are you really saying that you never need conditional compilation?"

"Indeed no, my child," the Guru smiled. "Conditional compilation will always be with us, even leaving aside the blessed 'include guard' idiom. There may well be times when an occasional #ifdef is required. In those rare situations, prefer to keep the conditional code in the header files and out of the main logic. And always test for features, never for platforms, for it is the truth that variations among compilers are rarely confined to a single platform."

"Features?" I quizzed. The Guru picked up the whiteboard marker once more:

// Not a good way:
#ifdef _WIN32

// much better way:
#ifndef HAVE_MEMBER_FN_TEMPLATES
"Now, my child," the Guru said softly as she glided away, "make the changes I have suggested, and then you will be ready to check in the files."

Notes

[1] With apologies to John Lennon.

[2] For any readers who might consider this a gratuitous slam against the open source movement, please note the slam against non-standards-conforming compilers at the end of the article.

[3] One notable example is C++ expert James Kanze, a moderator for the USENET group comp.lang.c++.moderated and a member of the ISO C++ Standards Committee, whose recent comments in comp.lang.c++.moderated inspired this article.

[4] Henry Spencer and Geoff Collyer. "#ifdef Considered Harmful, or Portability Experience with C News", Summer 1992 USENIX. Available online at <www.chris-lott.org/ resources/cstyle/ifdefs.pdf>.

About the Authors

Herb Sutter (www.gotw.ca) is convener of the ISO C++ standards committee, author of the acclaimed books Exceptional C++ and More Exceptional C++, and one of the instructors of The C++ Seminar (www.gotw.ca/cpp_seminar). In addition to his independent writing and consulting, he is also C++ community liaison for Microsoft.

Jim Hyslop is a senior software designer with over 10 years programming experience in C and C++. Jim works at Leitch Technology International Inc., where he deals with a variety of applications, ranging from embedded applications to Windows programs. He can be reached at jhyslop@ieee.org.


Related Reading


More Insights






Currently we allow the following HTML tags in comments:

Single tags

These tags can be used alone and don't need an ending tag.

<br> Defines a single line break

<hr> Defines a horizontal line

Matching tags

These require an ending tag - e.g. <i>italic text</i>

<a> Defines an anchor

<b> Defines bold text

<big> Defines big text

<blockquote> Defines a long quotation

<caption> Defines a table caption

<cite> Defines a citation

<code> Defines computer code text

<em> Defines emphasized text

<fieldset> Defines a border around elements in a form

<h1> This is heading 1

<h2> This is heading 2

<h3> This is heading 3

<h4> This is heading 4

<h5> This is heading 5

<h6> This is heading 6

<i> Defines italic text

<p> Defines a paragraph

<pre> Defines preformatted text

<q> Defines a short quotation

<samp> Defines sample computer code text

<small> Defines small text

<span> Defines a section in a document

<s> Defines strikethrough text

<strike> Defines strikethrough text

<strong> Defines strong text

<sub> Defines subscripted text

<sup> Defines superscripted text

<u> Defines underlined text

Dr. Dobb's encourages readers to engage in spirited, healthy debate, including taking us to task. However, Dr. Dobb's moderates all comments posted to our site, and reserves the right to modify or remove any content that it determines to be derogatory, offensive, inflammatory, vulgar, irrelevant/off-topic, racist or obvious marketing or spam. Dr. Dobb's further reserves the right to disable the profile of any commenter participating in said activities.

 
Disqus Tips To upload an avatar photo, first complete your Disqus profile. | View the list of supported HTML tags you can use to style comments. | Please read our commenting policy.
 

Video