Error Handling: The Sooner, The Better

rrors and exceptionsare two similar notions that differ in one crucial aspect: exceptions are a runtime concept, whereas errors are anomalous conditions that can and should be detected as early as possible?at compile-time or even during the preprocessing phase. The following sections show how to deal with errors during the preprocessing phase.


How do you detect anomalous and incorrect settings of the target environment as early as possible?


Use the preprocessor’s conditional expressions and the #errordirective.

What Can Go Wrong?
Many things can go wrong even before you start to build your application. Improper hardware settings, incorrect ABI (application binary interface), unsupported locales and character encodings, memory alignment issues, and lack of 64-bit addressingare instances of preconditions that cross-platform software projects must check. Normally, such projects use a defensive approach to detect errors as early as they can, instead of waiting for mysterious runtime crashes.

Error Detection
Suppose you’re developing an internalized application that requires Unicode support. The application is resilient enough to deal with two different types of Unicode encoding standards, namely UTF16 and UTF32. However, you want to ensure that an executable shall not be produced if the target environment doesn’t support Unicode. A few months ago, I showed how the new character types of C++09 are used for encoding and manipulating Unicode strings. Your project can check whether _Chart16_t and _Chart32_t are supported at the preprocessing stage by combing the #if preprocessor directive with the definedpredicate like this:

#if defined (__STDC_UTF_32__)# //do nothing#elif defined (__STDC_UTF_16__)# // UTF16 support is also fine#else //no Unicode support at all #error "Unicode support required for this application!"#endif

Let’s examine the preprocessor directives more carefully. The first line checks whether the macro __STDC_UTF_32__ is defined. If it is, the next line, which contains the null preprocessor directive #, is executed and the rest if the #if- block is skipped. If, however, __STDC_UTF_32__ isn’t defined, the next #elifdirective is evaluated.

Author’s Note: You can have an unlimited number of #elif statements in a single #if-#endif block but there can be only one #else per block.

Here again, if __STDC_UTF_16__ is defined, the next null preprocessor directive is executed and the rest of the block is skipped. If however neither __STDC_UTF_32__ nor __STDC_UTF_16__ is defined, it means that the current implementation doesn’t support the _Chart16_t and _Chart32_t types at all. In that case, the #errordirective is executed.

The #error directive is of special interest for you. It takes an optional quoted literal string containing a description of the problem. When the #errordirective is executed, compilation is aborted, and the literal string is displayed on the standard output and written to a log file, too. This way you guarantee that an executable file isn’t generated when it shouldn’t be.

When I build a project that contains the above #if block in an environment that doesn’t support the _Char16_t and _Char32_ttypes, the following diagnostic is issued:

c:documents and settingsDannymy documentsvisual studio 2005projectsunions	est.cpp(5) : fatal error C1189: #error : "Unicode support required for this application!"Build log was saved at "file://c:Documents and SettingsDannyMy DocumentsVisual Studio 2005ProjectsunionsDebugBuildLog.htm" 2005ProjectsunionsDebugBuildLog.htm" 

Notice that the IDE writes the #errormessage to a corresponding log file. This is handy if you want to examine why a nightly build failed for example.

Code Simplification
You can simplify the upper #if-#endifblock by collapsing the two conditions into a single statement:

#if defined (__STDC_UTF_32__) || defined (__STDC_UTF_16__)##else #error "Unicode support required for this application!"#endif

If you’re not fond of empty # directives, you can rewrite the conditional expression further as:

#if !defined (__STDC_UTF_32__) && !defined (__STDC_UTF_16__)  #error "Unicode support required for this application!"#endif

The point here is to show that the preprocessor recognizes the logical operators &&, !=, <, etc. so you can combine them in complex conditional expressions.

What #if
There are some differences between the if-statement of C and C++ and the #if preprocessor directive. Unlike the C++ if-statement, #if takes only a restricted subset of integral constant expressions. More specifically, sizeof expressions cannot be used in an #if directive. The reason for this is that sizeof requires the invocation of the compiler, whereas #ifis evaluated conceptually before compilation.

This restriction raises a problem. Suppose you want to ensure that your app is compiled only with 64-bit addressing mode. Seemingly, one could use the following expression to ensure that the size of a pointer is larger than 32 bits:

#if (sizeof (void*)<= sizeof(int32_t)) #error "this app requires 64-bit addressing"#endif

However, as previously explained, sizeof expressions cannot be used in this context. Therefore, the best way to detect such configuration problems during the preprocessing stage is to #define a macro symbol for every condition and then check whether that macro has been #defined. In the case of 64-bit addressing, there isn't a standard macro yet; each platform uses its own macro. For example, Windows Visual Studio 2005 automatically #defines the macro _Wp64when the project is compiled as a 64-bit Windows application. Therefore, you can use the following test to enforce 64-bit addressing mode for Windows apps:

#if !defined (_Wp64) #error "This application requires 64-bit addressing"#endif

The Importance of Early Detection
Many applications use the assert() macro (or worse yet, exceptions) to enforce constraints that can be checked much earlier. Using the #if and #errordirectives for the early detection of incorrect hardware configuration and compiler settings is a better strategy as it ensures that no executable is produced when it shouldn't. This way not only are you saving precious time and resources on building, distributing, and loading an executable that shouldn't have been produced in the first place, you're also protecting your clients from data corruption and security loopholes that a might arise due to a crash.

Share the Post:
Share on facebook
Share on twitter
Share on linkedin

Overview

Recent Articles: