C++ Preprocessor - #ifdef

An introduction to the C++ preprocessor, and how we can automatically prevent parts of our code from being included when we compile
This lesson is part of the course:

Intro to C++ Programming

Become a software engineer with C++. Starting from the basics, we guide you step by step along the way

3D art showing a character in a bar
Ryan McCombe
Ryan McCombe
Posted

Having all of our code in a single file is starting to become problematic. As we add more and more code, we really need to keep things more organised.

To accomplish this, we first need to familiarise ourselves with the preprocessor.

When we build our code, it is not sent directly to the compiler. Instead, it is first sent to the preprocessor. The preprocessor modifies our source code before the compiler sees it.

Translation and Translation Units

The modifications that occur to our source code before it is compiled is sometimes called translation. The files that are generated by translation are sometimes called translation units. It is these translation units that are sent for compilation.

The preprocessor does not modify our original code. It generates temporary copies of our files, and modifies those. These copies are removed after the compiler has completed. Therefore, we normally don't see the modifications the preprocessor does - it happens behind the scenes.

What the C++ Preprocessor Does

We can give instructions to the preprocessor by including specific instructions in our source code files. These instructions are called directives and, when the preprocessor encounters one, it reacts accordingly.

These pre-processor directives are very powerful. They are, in a sense, a programming language in their own right. They have variables, conditional statements, function-like syntax and more.

However, as the C++ standard evolved and added new options, the need for many of these directives have been removed or reduced.

In this introductory course, we will only cover the two aspects of the preprocessor that remain generally useful: the include directive, and conditional inclusion.

We'll look at the include directive in the next lesson.

In this lesson, we'll see how we can use a combination of directives to achieve conditional inclusion.

Test your Knowledge

What is the preprocessor?

Conditional Inclusion using #ifdef

A common use of the preprocessor is to determine what code is actually included in the software we build.

For example, we often want our software to behave differently when run by a developer, compared to a real world user.

The development version might show a lot more information to help us debug issues, and it might contain a lot of shortcuts to help us test different parts of the game quickly.

Rather than having a totally different project for each of the different versions, it's much easier to have a single project containing all of our code. Then, with the help of the preprocessor, we can generate multiple different versions of our program from the exact same code.

For example, we could generate two different versions (sometimes also called builds) of our software based on whether an option is toggled on or off prior to compilation. A toggle that makes this happen is sometimes called a build flag, or just a flag.

We will cover how to set a flag later in this lesson. For now, lets see how we could modify our code based on whether or not a specific flag was set.

We do that by wrapping the code between #ifdef and #endif directives:

void TakeDamage(int Damage) {
  Health -= Damage;
#ifdef IS_DEVELOPMENT_BUILD
  UseDeveloperCheats();
#endif
}

Now, if we build our software with the IS_DEVELOPMENT_BUILD flag defined, an additional line of code will be included in our build.

The opposite of #ifdef is #ifndef. This will include code if a flag is not defined:

void TakeDamage(int Damage) {
  Health -= Damage;
#ifndef IS_PUBLIC_BUILD
  UseDeveloperCheats();
#endif
}

We can also use #elif (which means "else if") and #else:

void CreateWindow() {
#ifdef IS_DEVELOPER_BUILD
  // Internal build
#elif IS_DEMO_BUILD
  // Demo build
#else
  // Public build
#endif
}

Other common uses for conditional inclusion might be when we are building for multiple platforms. For example, we might need a function to work slightly differently on Mac when compared to Windows.

Another use might be to ship different price tiers of our software where more expensive versions get additional features.

#ifdef Directives vs if Statements

A common question at this point is why would we ever need this. After all, we can just do the same thing with if statements.

The key difference is when the conditional check is done. Preprocessor directives are analysed at build time, whilst if statements are analysed at run time.

Most things can only be checked at run time, so if statements are generally going to be much more common and useful.

But, if something doesn't need to be checked at run time, we should consider using the preprocessor instead. Conditional inclusion has two big benefits over if statements:

  • Performance - an #ifdef directive is evaluated one time - when we build our software. An if statement is evaluated every time the function is called, by everyone who runs our software.
  • Security - if we include all the "secret" functionality in the code we send to users, an if statement won't keep it hidden. It is quite easy for someone to reverse engineer and see things we wanted to keep hidden. If we use #ifdef instead, the code is entirely removed from what we release.

Test your Knowledge

What is the purpose of the #ifdef, #ifndef, #elif, #else and #endif preprocessor directives?

C++ Macro Definitions with #define

We've seen how the conditional inclusion directives can remove code from our build based on the presence (or absence) of flags. We set these flags using the #define directive. For example:

#define IS_DEVELOPMENT_BUILD

Some things to note about #define are;

  • The things we define with this directive are typically called "macros" - eg, IS_DEVELOPMENT_BUILD is a macro
  • By convention, macro names are all upper case, and use _ as a separator
  • We do not end macros with a semi colon. The preprocessor is not using C++ code - it is using its own syntax

A macro is available to any other preprocessor directive that is used, from the line the macro was defined, until the end of the file.

Typically, when using these flags for the conditional inclusion use cases described above, we want it to be defined globally, for the entire project.

We do this through our tooling. For example, in Visual Studio, we can define macros globally under Project -> Properties -> C/C++ -> Preprocessor -> Preprocessor Definitions

A screenshot showing the preprocessor settings in Visual Studio

This integrates with Visual Studios build configuration system, allowing us to define a different set of macros for each configuration.

Build configurations are Visual Studio's way of letting us achieve the goal we described earlier in this section - to easily create different versions of our software from the same code base.

By default, our project was likely created with two configurations - Debug and Release - but we can delete, modify and create more as needed.

Test your Knowledge

How can we define a macro called IS_DEMO that we could use with an #ifdef directive?

Macros have some additional properties, too. They're not particularly useful, and are increasingly uncommon as more modern C++ features make them unnecessary. But, they're worth noting briefly, as you're still likely to encounter them.

Firstly, macros can perform text substition in our code. For example, the following directive will replace any occurance of NAME with the code to create an integer variable with a value of 4.

As such, the second line of code will be int MyVar { 4 }; when it reaches the compiler, thereby creating a variable.

#define CREATE_INT int MyVar { 4 };
CREATE_INT
cout << MyVar;

Secondly, macros can also accept arguments, to generate their replacement text dynamically. This can make them behave somewhat like functions.

The following version of the macro accepts two arguments to be used in the replacement. As such, the second line will become int Hello { 5 }; when it reaches the compiler, and 5 will be logged out.

#define CREATE_INT(Name, Value) int Name { Value };
CREATE_INT(Hello, 5)
cout << Hello;

There are additional interesting properties here, but going deep into what's possible with macros is is out of scope for this course. In addition, macros in general are becoming less useful over time with more modern C++ features get wider support.

For now, I'd just recommend remembering that these function-like macros exist.

You're unlikely to find many use cases for them, especially as a beginner. However, you'll likely find yourself using ones that other people created.

To understand what's going on in these scenarios, it's helpful to recognise that it's a macro you're using, rather than a C++ function.

A common way to identify when you're using a macro is that they tend to have an UPPERCASE_NAME, for example:

PROBABLY_A_MACRO(Hello, World)

An Unreal Engine Note

Unreal provides a lot of useful utilities in the form of macros, so you're likely to be using them quite heavily if you're writing C++ in that context.

For example, logging to the Unreal console is done using two function-like macros, UE_LOG and TEXT:

UE_LOG(LogTemp, Error, TEXT("Something went wrong!"))

Aside from conditional inclusion, the other main use for the preprocessor allows us to import code from other places into our files. We will introduce this in the next lesson!

Was this lesson useful?

Ryan McCombe
Ryan McCombe
Posted
This lesson is part of the course:

Intro to C++ Programming

Become a software engineer with C++. Starting from the basics, we guide you step by step along the way

The Preprocessor and the Linker
3D art showing a progammer setting up a development environment
This lesson is part of the course:

Intro to C++ Programming

Become a software engineer with C++. Starting from the basics, we guide you step by step along the way

Free, unlimited access!

This course includes:

  • 66 Lessons
  • Over 200 Quiz Questions
  • Capstone Project
  • Regularly Updated
  • Help and FAQ
Next Lesson

C++ Include Directive

Learn how we can split our code into multiple files, to keep everything organised. Then, use the include directive to automatically bring them back together for compilation.
3D art showing a character on a desert planet
Contact|Privacy Policy|Terms of Use
Copyright © 2023 - All Rights Reserved