Compiler Design and ConstructionUnit 128 min read
Macros, Preprocessing & Compiler Passes
Unit 12 of Compiler Design and Construction covers macro definitions, preprocessing directives, macro expansion rules, and the distinction between one-pass vs. multi-pass compilers, with real-world examples from C/C++ and practical traces of macro substitutions.
TAKEAWAYS:
- Macros are text substitutions performed before compilation, unlike functions which execute at runtime.
- The preprocessor handles
#define,#include, and conditional directives (#ifdef) before the compiler processes code. - One-pass compilers process input in a single scan (e.g., early C compilers), while multi-pass compilers (e.g., GCC) separate lexical/syntax analysis from code generation.
- Macro expansion can cause side effects (e.g.,
SQUARE(2+3)evaluates to(2+3)*(2+3)) and recursion issues (e.g.,#define CUB(x) x*x*x). - Advantages: Macros reduce code duplication; disadvantages: They obscure logic and can introduce bugs.
- Exam focus: Trace macro expansion step-by-step and compare one-pass vs. multi-pass compilers.
1. What Are Macros?
Macros are preprocessor directives that perform text substitution before compilation. Unlike functions, macros do not have a separate call stack or type safety. They are defined using #define and expanded by the preprocessor.
Macro Definition Syntax
#define MACRO_NAME(replacement_list) replacement_text
Example:
#define PI 3.14159
#define SQUARE(x) ((x) * (x))
Macro Expansion Process
The preprocessor replaces every occurrence of MACRO_NAME with replacement_text before the compiler sees the code.
Example Expansion:
printf("Area = %f", PI * SQUARE(5)); // After expansion:
printf("Area = %f", 3.14159 * ((5) * (5)));
2. Types of Macros
| Type | Syntax | Example | Key Feature |
|---|---|---|---|
| Object-like Macro | #define NAME replacement |
#define PI 3.14159 |
Simple text substitution. |
| Function-like Macro | #define NAME(args) replacement |
#define SQUARE(x) ((x)*(x)) |
Takes arguments; expands like a function. |
| Conditional Macro | #ifdef, #ifndef, #else |
#ifdef DEBUG |
Enables/disables code blocks. |
| Parameterized Macro | #define NAME(args) ... |
#define MAX(a,b) ((a)>(b)?(a):(b)) |
Evaluates arguments before expansion. |
3. Macro Expansion with Examples
Example 1: Object-like Macro
#define GREET "Hello, World!"
printf(GREET); // Expands to: printf("Hello, World!");
Example 2: Function-like Macro (with Pitfalls)
#define SQUARE(x) ((x) * (x))
int y = SQUARE(2+3); // Expands to: ((2+3) * (2+3)) → 25 (not 16!)
Why? Macros expand before evaluation. Parentheses prevent incorrect grouping.
Example 3: Conditional Macro (Debugging)
#define DEBUG
int x = 10;
#ifdef DEBUG
printf("Debug: x = %d\n", x); // Only compiled if DEBUG is defined.
#endif
4. Macro vs. Function
| Feature | Macro | Function |
|---|---|---|
| Execution Time | Preprocessing (compile-time) | Runtime |
| Type Safety | No (text substitution) | Yes (type-checked) |
| Overhead | None (text replacement) | Function call overhead |
| Argument Evaluation | Evaluated before expansion | Evaluated at call time |
| Recursion | Can cause infinite expansion | Safe (stack-limited) |
Example of Macro Pitfall (Recursion):
#define CUBE(x) x*x*x
int z = CUBE(2+1); // Expands to: 2+1*2+1*2+1 → 7 (not 27!)
Fix: Use parentheses:
#define CUBE(x) ((x)*(x)*(x)) // Now: ((2+1)*(2+1)*(2+1)) → 27
5. Preprocessing Directives
The preprocessor handles:
- Macro Definitions (
#define,#undef) - File Inclusion (
#include <file.h>or#include "file.h") - Conditional Compilation (
#ifdef,#ifndef,#else,#endif) - Line Control (
#line,#error)
Example: Conditional Compilation
#define VERSION 2
#if VERSION == 1
#include "old_code.h"
#else
#include "new_code.h"
#endif
6. One-Pass vs. Multi-Pass Compilers
| Feature | One-Pass Compiler | Multi-Pass Compiler |
|---|---|---|
| Phases | Single scan of input | Multiple scans (lexical → syntax → codegen) |
| Example | Early C compilers (e.g., cc in 1970s) |
GCC, Clang, MSVC |
| Advantages | Faster for simple languages | Better optimization, modular design |
| Disadvantages | Limited error recovery | Higher memory usage |
| Use Case | Embedded systems (resource-constrained) | General-purpose compilers |
Mermaid Diagram: Compiler Passes
flowchart TD
A["Source Code"] --> B["Preprocessor<br/>(#define, #include)"]
B --> C["Lexical Analyzer<br/>(Tokens)"]
C --> D["Syntax Analyzer<br/>(AST)"]
D --> E["Semantic Analyzer<br/>(Type Checking)"]
E --> F["Intermediate Code<br/>(Three-address code)"]
F --> G["Code Optimizer"]
G --> H["Code Generator<br/>(Machine Code)"]
H --> I["Linker<br/>(Executable)"]7. Real-World Applications of Macros
In the Real World
Linux Kernel (
#definefor Hardware Registers)- Macros like
#define GPIO_PIN_0 0x01map memory addresses to readable names. - Why? Avoids hardcoding hex values; improves maintainability.
- Macros like
OpenCV (
#define CV_PI 3.14159)- Predefined constants like
CV_PIare used in image processing functions. - Example:
circle(img, center, radius, CV_PI * radius, 2);
- Predefined constants like
Game Engines (Unity/Unreal)
#define MAX_PLAYERS 64sets limits at compile-time.- Why? Prevents runtime errors from exceeding array bounds.
Nepal’s eSewa (Conditional Macros for Feature Flags)
#ifdef DEBUG_MODEenables logging only in development builds.- Example:
if (DEBUG_MODE) logTransaction();
Khalti API (Header Guards)
#ifndef KHALTI_API_Hprevents multiple inclusions ofkhalti.h.- Why? Avoids "redefinition" compiler errors.
8. Worked Example: Full Macro Expansion
Given:
#define MAX(a,b) ((a)>(b)?(a):(b))
#define SQUARE(x) ((x)*(x))
int main() {
int x = MAX(3, SQUARE(2));
return 0;
}
Step-by-Step Expansion:
First Pass (Preprocessor):
- Replace
SQUARE(2)→((2)*(2))→4 - Replace
MAX(3, 4)→((3)>(4)?(3):(4))→4 - Final expanded code:
int main() { int x = ((3)>(4)?(3):(4)); return 0; }
- Replace
Compiler Processes:
- The expanded code is now parsed, type-checked, and compiled.
9. Common Macro Pitfalls & Fixes
| Pitfall | Example | Fix |
|---|---|---|
| Missing Parentheses | #define SQUARE(x) x*x |
#define SQUARE(x) ((x)*(x)) |
| Argument Evaluation Order | SQUARE(a++) → a*a++ (undefined) |
Use functions or parentheses |
| Macro Recursion | #define FACT(n) n*FACT(n-1) |
Use functions or iterative macros |
| Side Effects in Arguments | SQUARE(i++) → i*i++ (UB) |
Avoid macros for expressions with side effects |
10. Exam Tip
For macro expansion questions:
- Expand step-by-step, showing intermediate results.
- Parentheses matter! Always add them to avoid incorrect grouping.
- Trace conditional macros (
#ifdef) by showing which blocks are included/excluded.
For one-pass vs. multi-pass:
- One-pass: Faster, less error recovery (e.g., old C compilers).
- Multi-pass: Slower but modular (e.g., GCC’s
gcc -Efor preprocessing).
Common exam questions:
- Expand
#define MIN(a,b) ((a)<(b)?(a):(b))forMIN(x+1, y*2). - Differentiate macros from functions (execution time, type safety).
- Explain why
SQUARE(2+3)gives25(not16).
- Expand
Visual Summary:
mindmap
root((Macros & Preprocessing))
Definition
Text substitution before compilation
Types
Object-like
Function-like
Conditional
Expansion
Step-by-step replacement
Pitfalls
Missing parentheses
Side effects
Recursion
Real-World
Linux Kernel
OpenCV
eSewa
Compiler Passes
One-pass (fast, limited)
Multi-pass (modular, optimized)Based on the TU BSc CSIT syllabus for Compiler Design and Construction (CSC365), unit 12.
Discussion
Loading…