The random utterances of David Arno

#define and other C# preprocessor directives

What is it?

A set of directives that (mainly) affect how the compiler sees your code. The set of directives is:

    #region
    #endregion

    #define
    #undef
    #if
    #else
    #elif
    #endif

    #warning
    #error
    #line

I say mainly affects the compiler, as the first two – #region and #endregion – are completely ignored by the compiler. Instead these are used by the Visual Studio editor’s outlining feature.

The next six directives – #define through #endif – can be used to control conditional compilations of your code.

The last three directives – #warning, #error and #line – force the compiler to generate a warning and error in the first two instances and override the real file and line number details in the last instance.

Which C# version supports it?

Version 1 for .NET framework 1.1 and above.

Why use it?

Regions

If you use Visual Studio, you have likely come across regions already, but you may not have really noticed them. They are a very useful feature as they make it simpler to navigate around large sections of code.

Conditional Compilation

Conditional compilation directives make it easy to build varying versions of an application simply by varying the compiler flags. Methods can be disabled, debug calls can be removed from the code and functionality can be limited or enhanced as required. The use of conditional compilation enables you to leave debug logging in the source, even when building a release version for example. It can also be used, for example, to enable the same set of sources to be used to build a full version and a demo version of the same application.

Warnings Errors & Line Numbers

The ability to generate warnings and errors in your code might at first seem utterly useless. They have uses in a number of scenarios however. First, handy bits of “quick and dirty” code can be marked with a #warning. That way you are reminded of them every time you compile. Taking this scenario further, enable “treat warnings as errors” in the release version, and you will not be able to build the release code until you rewrite them. Next, you might use #warning in conjunction with conditional compilation to remind you that debug is enabled. Once again, the “treat warnings as errors” feature can prevent that debug appearing in release code. Finally a #error could be used to prevent the compilation of code using conflicting #defines. I cover each of these scenarios in examples below.

The #line directive has two main uses. First it can be used to hide sections of code from the debugger. This can be useful when debugging changes to existing code. Secondly, it is useful for code generation or modification tools. So for example, you might write a clever C# optimization utility. The utility simply copies invalid code into its optimized file. So when the compiler reports an error, you want it to reference back to the original file and line, rather than your optimized output. Here, #line comes into its own, as it can be used to tell the compiler to report different line numbers and file names to that which it is processing. Both of these uses are beyond the scope of this article, so I’ll cover them no further.

How to use it

Regions

The easiest way to understand how to use regions is to show them in use in Visual Studio. Consider the following snippet of code:

code1.png

It defines two regions, one within the other: “Some Methods” and “Some pointless code”. If you paste the above code into Visual Studio and click on the bottom [-], then the purpose becomes clear as the snapshot below shows:

code2.png

Conditional Compilation

Example 1: Enabling a debug logger via a #define (and compiler flag)
Consider a situation where you have a debug logger within your code. You obviously don’t want to ship it with the released code, so you may choose to use a #define to determine whether it runs or not:

#define DEBUG_LOGGER

class Program
{
    static void Main(string[] args)
    {
#if DEBUG_LOGGER
        DebugLogger.Enable();
#else
        DebugLogger.IgnoreMessages();
#endif
    }
}

As the code above stands, DebugLogger.Enable() will be called. Comment out line 1 and DebugLogger.IgnoreMessages() will be called instead. There is a big limitation with this approach though: DEBUG_LOGGER is only then defined in the file that contains the #define. It is possible to make a definition global. On the “Build” tab of the project’s properties, there is a field “Conditional compilation symbols”. If we add DEBUG_LOGGER there, as highlighted below in green, then it is defined for all source files in the project.

config1.png

It is worth pointing out at this stage that Visual Studio provides a couple of handy definitions automatically. As highlighted in red above,you can control whether DEBUG and TRACE are defined or not via the tick boxes. By default, both are defined for the debug build and only TRACE is defined for the release build.

Example 2: Using #undef to locally suppress a global definition
The disadvantage of using global definitions, is that modifying a projects properties causes a complete recompilation of the project source files. So if we have DEBUG_LOGGER defined in the properties and we want to temporarily suppress the debug messages, is there an easy way around it? Yes there is, use #undef. So with DEBUG_LOGGER globally defined, we change the code to:

#undef DEBUG_LOGGER

class Program
{
    static void Main(string[] args)
    {
#if DEBUG_LOGGER
        DebugLogger.Enable();
#else
        DebugLogger.IgnoreMessages();
#endif
    }
}

and DebugLogger.IgnoreMessages() will be run.

Example 3: Using conditional compilation to create application versions with different feature sets
Imagine I’ve written an application that I’ve imaginatively called ArnoApplication. ArnoApplication comes in a basic and advanced versions and there is a trial version, that shows off the advanced features for a limited time. I can then use conditional compilation to control which version is built as shown in the code snippet below.

class Program
{
    static void Main(string[] args)
    {
#if TRIAL_VERSION
        Advanced(true);
#elif ADVANCED_FEATURES
        Advanced(false);
#else
        Basic();
#endif
    }

    void Advanced(bool timeLimit)
    {
        if (timeLimit)
        {
            ...
        }
        ...
    }

    void Basic()
    {
        ...
    }
}

I can then use the solution Configuration Manager to define three configurations: Trial, Basic and Advanced and define appropriate compilation symbols in each to build the three versions of my application.

Warnings Errors & Line Numbers

Example 1: Using #warning to remind us of bad code
Consider the code below. Not only is the method pointless, it is confusing too as it returns a value different to its name. Now it’s unlikely you’d ever write such code in real life, but such pointless and confusing code tends to creep into all of our software at times. By marking it with a #warning, we are reminded of it every time we compile the code, and so are more likely to fix it.

#warning This is a pointless method. Fix before we ship
int Ten()
{
    int a = 9;
    return a;
}

Example 2: Using #warning to prevent debug code appearing in release builds
In the next example, I’ve added a debug logger check to my main class’ source file. So when building the debug version, I get a warning that I’m running the debug logger. The benefit of this feature then occurs when I set the release build to treat warnings as errors. If I accidentally enable the debug logger in the release build, the warning causes the compilation to fail.

#if DEBUG_LOGGER
#warning debug logger is enabled
#endif
class Program
{
    static void Main(string[] args)
    {
        ...
    }
}

Example 3: Using #error to prevent conflicting configurations
In the this example, imagine that I have written an application that is my answer to Visual Studio: Arno Studio. Arno Studio comes in two flavours: Amateur and Professional. The functionality of both is held in the Arno.Amateur.Functionality and Arno.Professional.Functionality namespaces respectively. I use conditional compilation to determine which namespace is exposed to the rest of the code when compiling each version. So my main class might look something like:

#if AMATEUR_VERSION
using Arno.Amateur.Functionality;
#elif PROFESSIONAL_VERSION
using Arno.Professional.Functionality;
#endif

class Program
{
    static void Main(string[] args)
    {
        ...
    }
}

However there is a problem. If I accidently define both AMATEUR_VERSION and PROFESSIONAL_EDITION, I get the amateur functionality. In a more complex situation where the two become defined, I could spend ages hunting down why I’m getting one configuration, when I was expecting another. To prevent this occurring, I make a simple modification to my main class’ code:

#if AMATEUR_VERSION && PROFESSIONAL_VERSION
#error Both amateur and professional versions are specified
#error Check AMATEUR_VERSION and PROFESSIONAL_VERSION defs
#endif

#if AMATEUR_VERSION
using Arno.Amateur.Functionality;
#elif PROFESSIONAL_VERSION
using Arno.Professional.Functionality;
#endif

class Program
{
    static void Main(string[] args)
    {
        ...
    }
}

Now if I accidentally define both, I get a clear compiler error that tells me what has gone wrong.

7 comments

7 Comments so far

  1. Tom August 6th, 2008 08:31

    Hello,

    thank you for this site.

    Beyond that information I would like to know, how to get the preprocessor code in Visual Studio…

  2. A. Sam March 9th, 2009 15:31

    Thanks for these useful topic.

  3. sujitha June 14th, 2009 02:59

    thanks… but i want to know how the ## preprocessor directive different from others?

  4. niranjan March 28th, 2010 06:38

    Thanks, very helpful, as is with a short and small description to understand….

  5. Yogesh August 13th, 2010 07:31

    Thanks this is very usefull for large application.
    Thanks a lot..

    Yogesh Sharma

  6. Valeriano Cossu August 27th, 2010 14:05

    Very good explanation. Thanks!

  7. James Lavelle December 2nd, 2010 06:38

    This was EXACTLY what I needed an example for. Thanks!!!