The Preprocessor: Conditionals, Feature Detection, Pragmas
This chapter covers conditional compilation (#if, #ifdef, etc.), feature detection, include guards, and #pragma. These are the preprocessor's tools for portability and configuration.
Why This Matters
Conditional compilation lets one codebase support multiple platforms, standards, and build configurations. Include guards prevent duplicate declarations. Feature detection with predefined macros is the standard way to write portable C.
Prerequisites
c.core.36— object-like macros and#include.
Core Concept
Conditional directives
#if expression /* integer constant expression */
#ifdef NAME /* true if NAME is defined */
#ifndef NAME /* true if NAME is not defined */
#elif expression
#else
#endif
The #if expression is evaluated by the preprocessor and must be an integer constant expression (macros are expanded first; undefined identifiers are replaced by 0).
Include guards
#ifndef MY_HEADER_H
#define MY_HEADER_H
/* header contents */
#endif
This ensures the header is included at most once per translation unit.
Feature detection
Predefined macros let you detect the compiler and standard:
#if defined(__STDC_VERSION__) && __STDC_VERSION__ >= 201112L
/* C11 or later */
#endif
__STDC_VERSION__ is 201112L for C11, 201710L for C17, 202311L for C23 (preliminary). Compiler-specific macros (__GNUC__, __clang__, _MSC_VER) identify the implementation.
#pragma
#pragma is an implementation-defined directive. Some pragmas are standardized (e.g., #pragma once is widely supported but not ISO C; #pragma STDC ... is the standard prefix). Pragmas control compiler behavior (packing, warnings, optimization).
Syntax
#if expr
#ifdef NAME
#ifndef NAME
#elif expr
#else
#endif
#pragma once
#pragma STDC ...
Examples
Include guard
#ifndef POINT_H
#define POINT_H
struct Point { int x; int y; };
#endif
Feature detection
#include <stdio.h>
int main(void)
{
#if defined(__STDC_VERSION__) && __STDC_VERSION__ >= 201112L
puts("C11 or later");
#else
puts("pre-C11");
#endif
return 0;
}
Compiler-specific code
#if defined(__GNUC__)
# define UNUSED __attribute__((unused))
#elif defined(_MSC_VER)
# define UNUSED
#else
# define UNUSED
#endif
How It Works
The preprocessor evaluates the controlling expression (after macro expansion) and includes or excludes the corresponding block. Excluded code is not compiled. This happens before the compiler proper sees the translation unit.
Variations
#if vs. #ifdef
#ifdef NAME checks only whether NAME is defined (regardless of value). #if NAME checks the *value* (with undefined names evaluating to 0). Use defined(NAME) inside #if for combined checks:
#if defined(A) && !defined(B)
#error and #warning
#error "unsupported platform"
#warning "deprecated path" /* #warning is not ISO C (widely supported) */
#error causes a compile-time error with the message; #warning is a non-standard warning.
Common Mistakes
- Using
#ifwith a runtime variable (preprocessor only sees constants/macros). - Forgetting
#endif(unbalanced conditional). - Relying on
#pragma oncefor portability (include guards are standard). - Using
#ifdefwhere#if defined(...)with a value check is needed.
Undefined Behavior
- Conditional compilation itself does not produce UB, but it can select code
that does. The preprocessor directives are well-defined.
Portability
- Include guards are fully portable.
#pragma onceis widely supported but non-standard.- Predefined macro names vary by compiler; detect features using standard
macros where possible (__STDC_VERSION__).
Under the Hood
Conditional compilation is pure preprocessing; excluded code never reaches the compiler. This means you can use platform-specific syntax inside #if blocks without breaking other platforms.
Practical Usage
- Always wrap headers in include guards.
- Use feature detection to write code that compiles across standards and
compilers.
- Use
#errorto fail clearly on unsupported configurations. - Keep
#pragmausage isolated and documented, since it is
implementation-defined.
Exercises
1. Write a header with an include guard and include it twice in one TU to verify no duplicate errors. 2. Write a program that prints which C standard it is compiled under using __STDC_VERSION__. 3. Use #if to select between two implementations of a function based on a macro. 4. Demonstrate #error by triggering it.
Deep Challenge
Write a portable header that selects an appropriate UNUSED-style annotation for GCC, Clang, MSVC, and "unknown compiler," using only conditional compilation and predefined macros. Explain each branch and the portability limits.
Related Concepts
c.core.39— multi-file programs and headers.c.build.8— compiler flags.c.core.36— includes and macros.
References
- ISO/IEC 9899:2018 §6.10.1 (conditional inclusion), §6.10.6 (#pragma),
§6.10.5 (#error).
Verification
- Include guards are standard.
VERIFIED __STDC_VERSION__values.VERIFIED#pragma onceis non-standard but widely supported.VERIFIED- No example was executed during generation unless noted. `Execution not
verified.`
Progress
- [ ] Read
- [ ] Understand
- [ ] Complete examples
- [ ] Complete exercises
- [ ] Complete deep challenge
Concept checkboxes
- [ ] #if/#ifdef/#ifndef/#elif/#else/#endif
- [ ] Include guards
- [ ] Feature detection
- [ ] #error/#warning
- [ ] #pragma
Mastery levels
| Concept | Current level (0–8) | Target level |
|---|---|---|
| c.pp.conditional | 0 | 5 |
| c.pp.pragma | 0 | 4 |