SrcForge

Lesson 61

Preprocessor Directives

Covers #include, #define, #undef, conditional compilation, #pragma, #error, header guards, and best practices for using the C++ preprocessor.

Lesson content

What are Preprocessor Directives in C++?

Preprocessor Directives in C++ are special instructions processed by the C++ preprocessor before the actual compilation begins. These directives help include header files, define constants, create macros, control conditional compilation, and manage source code organization.

Unlike normal C++ statements, preprocessor directives begin with the # (hash) symbol and are executed before the compiler translates your code into machine language.

Think of the preprocessor as an editor preparing a manuscript before it is printed. It inserts required files, replaces predefined words with values, and removes unnecessary sections of code before the compiler sees the program.

Why do they exist?

Preprocessor directives automate repetitive tasks and improve code organization. They allow programmers to include library files, define symbolic constants, create reusable macros, enable or disable sections of code, and prevent multiple inclusion of header files.

Real-world use cases

  • Including standard and custom header files
  • Creating header guards
  • Debugging programs
  • Platform-specific compilation
  • Defining compile-time constants
  • Conditional feature enabling
  • Writing portable libraries

Prerequisites

  • Variables and data types
  • Functions
  • Basic program structure
  • Header files
  • Basic compilation process (helpful)

What is the C++ preprocessor?

The preprocessor is a program that runs before the compiler. It processes all preprocessor directives and generates the modified source code that the compiler compiles.

#include <iostream>

The preprocessor replaces this line with the contents of the iostream header file before compilation.

Characteristics of preprocessor directives

  • Begin with the # symbol
  • Do not end with a semicolon (;)
  • Processed before compilation
  • Can span multiple source files
  • Mostly perform text substitution

Common preprocessor directives

  • #include — includes header files
  • #define — defines macros or constants
  • #undef — removes a macro definition
  • #ifdef — checks if a macro is defined
  • #ifndef — checks if a macro is not defined
  • #if — conditional compilation
  • #elif — additional conditional check
  • #else — alternative conditional block
  • #endif — ends a conditional block
  • #pragma — compiler-specific instructions
  • #error — generates a compile-time error
  • #line — changes line number information

#include directive

The #include directive inserts the contents of another file into the current source file. Angle brackets are used for standard library headers, and double quotes are used for user-defined header files.

// System header
#include <iostream>

// User header
#include "Student.h"

#define directive

The #define directive defines a macro or symbolic constant. Whenever the macro name appears, the preprocessor replaces it with its defined value.

#define PI 3.14159

// Function-like macro
#define SQUARE(x) ((x) * (x))

Be careful with macros because they perform simple text replacement and do not provide type checking.

#undef directive

The #undef directive removes a previously defined macro.

#define MAX 100

#undef MAX

// MAX is no longer available after this point

Conditional compilation

Conditional directives allow parts of the code to be compiled only if certain conditions are met.

// #ifdef
#ifdef DEBUG
cout << "Debug Mode";
#endif

// #ifndef (commonly used in header guards)
#ifndef MYHEADER_H
#define MYHEADER_H

// Header content

#endif

// #if, #elif, #else
#define VERSION 2

#if VERSION == 1
// Version 1 code
#elif VERSION == 2
// Version 2 code
#else
// Default code
#endif

#pragma directive

The #pragma directive provides compiler-specific instructions.

#pragma once

This prevents a header file from being included multiple times and is widely supported by modern compilers.

#error directive

The #error directive stops compilation with a custom error message. It is useful for enforcing build requirements.

#error Compiler version not supported.

Example 1: Using #include

#include <iostream>
using namespace std;

int main()
{
    cout << "Hello, World!";

    return 0;
}

// Output: Hello, World!

Example 2: Using #define

#include <iostream>
using namespace std;

#define PI 3.14159

int main()
{
    double radius = 5.0;

    double area = PI * radius * radius;

    cout << area << endl;

    return 0;
}

// Output: 78.5398

Example 3: Function-like macro

#include <iostream>
using namespace std;

#define SQUARE(x) ((x) * (x))

int main()
{
    int number = 6;

    cout << SQUARE(number) << endl;

    return 0;
}

// Output: 36

Example 4: Conditional compilation

#include <iostream>
using namespace std;

#define DEBUG

int main()
{
#ifdef DEBUG
    cout << "Debug Mode Enabled" << endl;
#endif

    cout << "Program Running";

    return 0;
}

// Output:
// Debug Mode Enabled
// Program Running

Example 5: Header guard

// MyHeader.h
#ifndef MYHEADER_H
#define MYHEADER_H

void display();

#endif

// main.cpp
#include "MyHeader.h"

int main()
{
    display();

    return 0;
}

The header guard ensures that MyHeader.h is included only once, preventing duplicate declarations.

Common mistakes and pitfalls

  • Adding a semicolon after #define. Fix: #define PI 3.14 (no semicolon)
  • Using quotes instead of angle brackets for standard headers. Fix: #include <iostream>
  • Forgetting #endif. Fix: always close conditional directives
  • Using macros for typed constants. Fix: prefer constexpr for constants
  • Defining unsafe macros without parentheses. Fix: wrap macro parameters in parentheses
// Wrong
#define SQUARE(x) x * x

cout << SQUARE(2 + 3);
// Output: 11

// Correct
#define SQUARE(x) ((x) * (x))

cout << SQUARE(2 + 3);
// Output: 25

Best practices

  • Use #include only for required header files
  • Prefer constexpr over #define for constants whenever possible
  • Use inline functions instead of function-like macros
  • Always use parentheses in macro definitions
  • Use #pragma once or header guards to prevent multiple inclusion
  • Keep conditional compilation simple and easy to understand
  • Avoid excessive use of compiler-specific #pragma directives
  • Use descriptive macro names in uppercase (for example, MAX_BUFFER_SIZE)

When not to use this

Avoid preprocessor directives when a constexpr variable can replace a macro constant, when an inline function is safer than a function-like macro, when templates provide a better generic solution, or when excessive conditional compilation makes code difficult to maintain.

Summary

  • Preprocessor Directives in C++ are processed before compilation
  • Every directive starts with the # symbol
  • #include inserts the contents of header files
  • #define creates macros and symbolic constants
  • #undef removes macro definitions
  • Conditional directives (#if, #ifdef, #ifndef, #else, #elif, #endif) control which code is compiled
  • #pragma once and header guards prevent multiple header inclusion
  • Prefer modern C++ features such as constexpr and inline functions over macros whenever possible

Frequently asked questions

What are preprocessor directives in C++?

Preprocessor directives are special instructions that begin with # and are processed before compilation. They control file inclusion, macros, and conditional compilation.

What is the difference between #include <...> and #include "..."?

Angle brackets are used for standard library headers, and double quotes are used for user-defined header files. The compiler searches for them in different locations.

Why is constexpr preferred over #define for constants?

constexpr provides type safety, respects C++ scope rules, and is checked by the compiler, whereas #define performs only text substitution.

What is a header guard?

A header guard uses #ifndef, #define, and #endif to ensure that a header file is included only once during compilation, preventing duplicate definitions.

What is #pragma once?

#pragma once is a compiler directive that also prevents multiple inclusion of a header file. It is simpler than traditional header guards and is supported by most modern C++ compilers.