Question
For cross-platform C or C++ code that must compile on macOS, iOS, Linux, and Windows, how can the target operating system be detected reliably during preprocessing?
Short Answer
You will learn how C preprocessors select platform-specific code using compiler-defined macros. You will also learn why macOS and iOS require special handling, how to organize checks safely, and why platform detection is not part of standard C.
Concept
The C preprocessor runs before C code is compiled. It can include or exclude sections of a source file with directives such as #if, #elif, and #endif.
Compilers commonly define macros that describe their target platform. For example:
- Windows targets define
_WIN32. - Linux targets commonly define
__linux__. - Apple targets define
__APPLE__.
However, these macros are mostly compiler and platform conventions, not rules defined by the ISO C standard. A portable C program cannot use one standard macro that identifies every operating system.
Apple platforms need an extra step: both macOS and iOS are Apple targets, so both can define __APPLE__. After detecting an Apple target, include Apple's TargetConditionals.h header and inspect its TARGET_OS_* macros.
Platform checks matter when code genuinely depends on an operating-system feature, such as file paths, process APIs, dynamic libraries, or native user-interface integration. Keep these checks small and isolate them behind your own project interface when possible.
Mental Model
Think of preprocessing as packing instructions for several destinations.
- A compiler target is the shipping label: Windows, Linux, or an Apple platform.
#if defined(...)is a sorting machine that sends the matching instructions into the final source file.- Code in non-matching branches is removed before the compiler checks it.
For Apple, __APPLE__ says only “this package goes somewhere in the Apple ecosystem.” TargetConditionals.h is the detailed address that tells you whether that destination is iOS or macOS.
Syntax and Examples
Use defined(MACRO) inside #if or #elif to test whether a compiler-defined macro exists.
#if defined(_WIN32)
/* Windows-specific code */
#elif defined(__linux__)
/* Linux-specific code */
#elif defined(__APPLE__)
/* An Apple platform; determine which one next. */
#else
/* An unsupported or unknown platform. */
#endif
A practical cross-platform header can convert platform-specific macros into project-owned macros:
/* platform.h */
#if defined(_WIN32)
#define PROJECT_WINDOWS 1
#elif defined(__APPLE__)
#include <TargetConditionals.h>
#if TARGET_OS_IPHONE
#define PROJECT_IOS 1
#elif TARGET_OS_OSX
#define PROJECT_MACOS 1
#else
#error "Unsupported Apple platform"
#endif
#elif defined(__linux__)
#define PROJECT_LINUX 1
#else
#error "Unsupported operating system"
#endif
Other source files can now use PROJECT_WINDOWS, PROJECT_IOS, PROJECT_MACOS, and PROJECT_LINUX rather than repeatedly checking compiler-specific names.
On Windows, use _WIN32 for both 32-bit and 64-bit Windows. Despite its name, _WIN32 is also defined by typical 64-bit Windows compilers. Use _WIN64 only when you specifically need to distinguish 64-bit Windows.
Step by Step Execution
Consider this program when compiled for an iOS target:
#include <stdio.h>
#if defined(_WIN32)
#define PLATFORM_NAME "Windows"
#elif defined(__APPLE__)
#include <TargetConditionals.h>
#if TARGET_OS_IPHONE
#define PLATFORM_NAME "iOS"
#elif TARGET_OS_OSX
#define PLATFORM_NAME "macOS"
#endif
#elif defined(__linux__)
#define PLATFORM_NAME "Linux"
#endif
int main(void) {
printf("Building for %s\n", PLATFORM_NAME);
return 0;
}
For an iOS build, preprocessing proceeds as follows:
_WIN32is not defined, so the Windows branch is discarded.__APPLE__is defined, so the Apple branch is selected.TargetConditionals.hprovides Apple target macros.TARGET_OS_IPHONEis true for an iOS target, soPLATFORM_NAMEbecomes"iOS".- The macOS and Linux branches are discarded.
- The compiler receives code effectively equivalent to:
#include <stdio.h>
#define PLATFORM_NAME "iOS"
int main(void) {
printf("Building for %s\n", PLATFORM_NAME);
return 0;
}
Real World Use Cases
- File-system behavior: choose Windows path handling or POSIX-oriented behavior where necessary.
- Native APIs: include Windows headers on Windows and Apple framework headers only on Apple targets.
- Dynamic libraries: use platform-specific library-loading implementations behind one shared interface.
- Build diagnostics: stop compilation with
#errorwhen an unsupported platform is targeted. - Mobile versus desktop behavior: compile iOS-only integrations separately from macOS desktop integrations.
- Testing and logging: report the compiled target platform in diagnostics or test output.
Prefer checking for a required capability when you can. For example, if you only need a compiler feature, test that feature rather than assuming every Linux compiler supports it in the same way.
Real Codebase Usage
In production code, put platform detection in one dedicated header, often named platform.h or config_platform.h. Translate external compiler macros into stable project names.
/* platform.h */
#if defined(_WIN32)
#define PROJECT_PLATFORM_WINDOWS 1
#elif defined(__linux__)
#define PROJECT_PLATFORM_LINUX 1
#else
#define PROJECT_PLATFORM_OTHER 1
#endif
Then keep platform-specific code behind a small interface:
/* system_paths.c */
#include "platform.h"
const char *temporary_directory(void) {
#if defined(PROJECT_PLATFORM_WINDOWS)
return "C:\\Temp";
#elif defined(PROJECT_PLATFORM_LINUX)
return "/tmp";
#else
return "/tmp";
#endif
}
Useful project practices include:
- Centralization: avoid scattering
_WIN32and__linux__throughout the codebase. - Early failure: use
#errorfor targets your project cannot support. - Capability checks: test for available headers, functions, or compiler extensions when that is the real requirement.
- Build configuration: let CMake, Meson, Autoconf, or another build system detect libraries and features, then generate configuration macros.
- Minimal branches: isolate native implementation files rather than wrapping large portions of application logic in preprocessor directives.
Common Mistakes
Assuming _WIN32 means only 32-bit Windows
This is incorrect:
#if defined(_WIN32)
/* 32-bit Windows only -- incorrect assumption */
#endif
_WIN32 is normally defined for both 32-bit and 64-bit Windows targets. Test _WIN64 only if pointer size or another 64-bit-specific detail truly matters.
Treating __APPLE__ as macOS only
This is incomplete:
#if defined(__APPLE__)
#define PLATFORM_NAME "macOS" /* May also be iOS. */
#endif
On Apple targets, include TargetConditionals.h and test TARGET_OS_IPHONE or TARGET_OS_OSX.
Testing Apple macros before including Apple definitions
TARGET_OS_IPHONE comes from an Apple header. Do not assume it exists in a source file that has not included the relevant header.
#if TARGET_OS_IPHONE /* May be undefined here. */
#endif
Instead, first identify Apple with __APPLE__, then include <TargetConditionals.h>.
Using nonstandard macro names without checking the compiler
Some compilers may provide aliases such as linux or __linux, but __linux__ is the commonly used spelling. Check your compiler's predefined macro documentation or inspect its macro output when supporting unusual toolchains.
Forgetting an unknown-platform branch
Without a final branch, unsupported builds may silently use the wrong fallback.
#else
#error "This operating system is not supported"
#endif
Detecting an OS when a feature check is better
An OS check does not guarantee that a particular library, API version, or compiler extension exists. Use your build system or feature-test macros when the code depends on a specific capability.
Comparisons
| Need | Best approach | Why |
|---|---|---|
| Select Windows-only code | #if defined(_WIN32) | Covers typical 32-bit and 64-bit Windows targets. |
| Select Linux-only code | #if defined(__linux__) | Common compiler-defined Linux target macro. |
| Select any Apple target | #if defined(__APPLE__) | Identifies Apple targets, but does not distinguish them. |
| Distinguish iOS and macOS | #include <TargetConditionals.h> and TARGET_OS_* | Apple-supported target classification. |
| Select 64-bit Windows specifically | #if defined(_WIN64) | Use only when 64-bit Windows differs materially. |
| Check whether an API exists | Build-system or feature check | More accurate than guessing from the OS. |
#ifdef NAME and #if defined(NAME) are similar for a simple existence test:
#ifdef _WIN32
/* Valid */
#endif
#if defined(_WIN32)
/* Also valid; easier to combine with other conditions. */
#endif
defined(...) is often preferred in complex conditions:
#if defined(__linux__) && defined(__x86_64__)
/* Linux on x86-64 */
#endif
Cheat Sheet
/* Windows: 32-bit or 64-bit */
#if defined(_WIN32)
#endif
/* 64-bit Windows only */
#if defined(_WIN64)
#endif
/* Linux */
#if defined(__linux__)
#endif
/* Apple platform, then refine the target */
#if defined(__APPLE__)
#include <TargetConditionals.h>
#if TARGET_OS_IPHONE
/* iOS target */
#elif TARGET_OS_OSX
/* macOS target */
#else
/* Another Apple platform or unsupported target */
#endif
#endif
- Preprocessor platform macros are not standardized by ISO C.
- Use
defined(MACRO)to test whether a macro exists. _WIN32includes 64-bit Windows;_WIN64means 64-bit Windows.__APPLE__is not enough to identify macOS versus iOS.- Keep detection in one project header and define project-owned macros.
- Add
#errorfor platforms the project does not support. - Prefer feature detection when your real question is “is this API available?”
FAQ
Is there a standard C macro for detecting the operating system?
No. ISO C does not define universal operating-system detection macros. These macros come from compilers and platform SDKs.
Why does 64-bit Windows define _WIN32?
The macro indicates the Windows API family rather than strictly the CPU bit width. Typical 64-bit Windows builds define both _WIN32 and _WIN64.
How do I distinguish macOS from iOS in C?
First test __APPLE__, include <TargetConditionals.h>, then test TARGET_OS_OSX for macOS or TARGET_OS_IPHONE for iOS.
Does __APPLE__ mean the code is running on an Apple device?
It describes the compilation target, not a runtime detection result. Preprocessor checks happen while building the program.
Can I use #ifdef __linux__ instead of #if defined(__linux__)?
Yes. Both test whether the macro is defined. #if defined(...) is convenient when combining several conditions.
Should every source file check _WIN32 and __linux__ directly?
Usually no. Put detection in a single project header and expose project-specific macros or platform-neutral functions.
How can I see macros defined by my compiler?
Most compilers have an option to output predefined macros. Consult the compiler documentation, especially when using a cross-compiler or an uncommon toolchain.
Mini Project
Description
Create a small C program that reports the operating system it was compiled to target. The project demonstrates centralized preprocessor detection and the Apple-specific distinction between iOS and macOS.
Goal
Build a program that prints Windows, Linux, macOS, or iOS based on the compiler target.
Requirements
- Create a header that determines one supported target platform at preprocessing time.
- Use
_WIN32for Windows and__linux__for Linux. - Use
__APPLE__andTargetConditionals.hto distinguish iOS from macOS. - Define one
PROJECT_PLATFORM_NAMEmacro for the selected platform. - Produce a clear compilation error for unsupported targets.
Keep learning
Related questions
2D Array Loop Order and Cache Performance in C
Learn why swapping nested loops changes 2D array performance in C, using row-major memory layout, cache locality, and practical benchmarks.
Array-to-Pointer Conversion in C and C++ Explained
Learn what array-to-pointer conversion means in C and C++, how array decay works, and how it differs from a pointer to an array.
Building More Fault-Tolerant Embedded C++ Applications for Radiation-Prone ARM Systems
Learn practical C++ and compile-time techniques to reduce soft-error damage in embedded ARM systems exposed to radiation.