C / TYPES AND REPRESENTATION
limits.h and querying your platform at compile time
Read your platform's integer ranges from limits.h and use them in #if and static_assert to pick types and reject unsuitable targets.
What you will learn
- Branch on capability with #if INT_MAX >= N instead of assuming a fixed width
- Convert a size into a bit count with sizeof(T) * CHAR_BIT, never by multiplying by 8
- Guard assumptions with static_assert, which sees sizeof and types where #if cannot
- Detect whether plain char is signed on this target with #if CHAR_MIN < 0
Understanding limits.h and querying your platform at compile time
limits.h contains no functions and no code, only macros that expand to integer constants describing the implementation you are compiling for: CHAR_BIT, SCHAR_MIN, UCHAR_MAX, CHAR_MIN and CHAR_MAX, then MIN/MAX pairs for short, int, long and long long alongside the unsigned MAX values. Those numbers were fixed when your compiler was built for its target, so reading them costs nothing at runtime; by the time the compiler proper sees your source, INT_MAX is literally the token 2147483647. Treat the header as the compiler's own description of the arithmetic it is about to generate, written in a form your source code can read.
This matters because #if is evaluated during preprocessing, when types and typedefs do not exist yet. A #if can only evaluate integer constant expressions built from literals, character constants and macro expansions, and any identifier still standing after macro expansion is replaced by 0 — which is why #if sizeof(long) == 8 is a syntax error while #if MY_TYPO == 8 is quietly false. Since limits.h macros expand to bare literals, they survive that stage, and that makes the range of a type the one property you can interrogate before compilation begins.
Phrase the tests as requirements rather than measurements. The standard promises only lower bounds — CHAR_BIT is at least 8, INT_MAX at least 32767, LONG_MAX at least 2147483647 — so #if LONG_MAX >= 10000000000 asks the question you actually care about, while #if LONG_MAX == 9223372036854775807 just asserts a guess about your favourite machine. Before C23 the standard also permitted ones' complement and sign-magnitude, which is why the guaranteed lower limit is -32767 and not -32768, and why INT_MIN + INT_MAX == -1 is the portable way to ask whether the target is two's complement. For assumptions that genuinely need type information, such as sizeof(int) == 4 or a struct layout, use static_assert from assert.h: it is checked after preprocessing, sees real types, and turns a wrong platform into a build failure instead of a runtime surprise.
<stdio.h>
<limits.h>
INT_KIND
INT_KIND
INT_KIND
CHAR_KIND
CHAR_KIND
int main(void)
{
printf("CHAR_BIT = %d\n", CHAR_BIT);
printf("plain char = %s, %d .. %d\n", CHAR_KIND, CHAR_MIN, CHAR_MAX);
printf("int = %s, %d .. %d\n", INT_KIND, INT_MIN, INT_MAX);
printf("UINT_MAX = %u\n", UINT_MAX);
printf("LONG_MAX = %ld\n", LONG_MAX);
printf("two's complement int: %d\n", INT_MIN + INT_MAX == -1);
return 0;
}
limits.h publishes the target's integer ranges as plain integer constants, which is what makes range the only property of a type the preprocessor can test.
Worked examples
A compile-time contract with static_assert
Records the assumptions a file makes about the target so an unsuitable platform fails to build rather than misbehaving.
<assert.h>
<limits.h>
<stdio.h>
static_assert(CHAR_BIT == 8, "this file packs data assuming 8-bit bytes");
static_assert(INT_MAX >= 2147483647, "the frame counter needs a 32-bit int");
static_assert(sizeof(int) * CHAR_BIT == 32, "int must be exactly 32 bits wide");
int main(void)
{
printf("compile-time checks passed\n");
printf("int occupies %zu bytes = %zu bits\n",
sizeof(int), sizeof(int) * CHAR_BIT);
return 0;
}
Example explained
Line 1static_assert is checked by the compiler, not the preprocessor, so sizeof(int) is legal here and would be a syntax error inside #if.
Line 2INT_MAX >= 2147483647 states a need; it passes on any target whose int is 32 bits or wider, including a 64-bit int.
Line 3CHAR_BIT is what turns a size in bytes into a size in bits; on a DSP with 16-bit bytes the third assertion fires and the build stops.
Line 4The two-argument form works in C11 with assert.h included, and the message is printed verbatim in the diagnostic when the test fails.
Why INT_MIN is spelled (-INT_MAX - 1)
Shows that the literal -2147483648 does not have type int, which is the reason limits.h never writes INT_MIN that way.
<limits.h>
<stdio.h>
int main(void)
{
printf("sizeof INT_MIN = %zu\n", sizeof INT_MIN);
printf("sizeof -2147483648 = %zu\n", sizeof (-2147483648));
printf("values are equal = %d\n", INT_MIN == -2147483648);
printf("-(long)INT_MIN = %ld\n", -(long)INT_MIN);
return 0;
}
Example explained
Line 12147483648 exceeds INT_MAX, so the decimal constant takes the first type that can hold it (long on this target) and the unary minus does not change that type.
Line 2limits.h therefore defines INT_MIN as (-INT_MAX - 1), an expression whose type is exactly int, so printing it with %d is correct.
Line 3The comparison is 1 because the int is converted to long before comparing; only the types differ, and it is the type that breaks printf and widens later arithmetic.
Line 4-(long)INT_MIN widens before negating; negating INT_MIN as an int overflows and is undefined behaviour, which is also why abs(INT_MIN) is not safe.
Choosing a type from a required range
Selects a signed type and its matching printf conversion by testing ranges in the preprocessor.
<limits.h>
<stdio.h>
typedef int counter_t;
COUNTER_NAME
COUNTER_FMT
typedef long counter_t;
COUNTER_NAME
COUNTER_FMT
typedef long long counter_t;
COUNTER_NAME
COUNTER_FMT
int main(void)
{
counter_t bytes_sent = 10000000000;
printf("counter type: %s\n", COUNTER_NAME);
printf("value: " COUNTER_FMT "\n", bytes_sent);
return 0;
}
Example explained
Line 1The chain compares against ranges because the preprocessor has no notion of "8 bytes"; LONG_MAX is the only visible fact about long.
Line 2On 64-bit Linux LONG_MAX is about 9.2e18 so the long branch wins; on 64-bit Windows LONG_MAX is 2147483647 and the same source falls through to long long.
Line 3The conversion specifier is defined in the same branch as the typedef, so the format string can never disagree with the argument's type.
Line 4#if arithmetic is done in at least 64-bit intmax_t since C99, so the literal 10000000000 is compared exactly even on a target where long is 32 bits.
Important notes
The values describe the target the compiler builds for, not the machine you run on: cross-compiling for an 8-bit AVR from an x86-64 host reports CHAR_BIT 8 but INT_MAX 32767.
limits.h covers integer types only — FLT_MAX, DBL_DIG and friends live in float.h — and C23 adds width macros such as INT_WIDTH and LONG_WIDTH that give bit counts directly in #if.
Common mistakes
Writing #if sizeof(int) == 4: preprocessing runs before types exist, so gcc reports 'missing binary operator before token (' and the file does not compile at all; the test must be written against INT_MAX.
Forgetting #include <limits.h>, or misspelling a macro as INT_MAXX: an unknown identifier in #if becomes 0, the condition is silently false, and the fallback branch is compiled with no diagnostic unless you build with -Wundef.
Assuming CHAR_MIN is -128: on ARM and PowerPC plain char is unsigned and CHAR_MIN is 0, so a test like if (c < 0) for bytes above 127 never fires and the code silently misreads binary data.
Try it yourself
Change, predict, then run
Print sizeof(T) * CHAR_BIT for char, short, int, long and long long, then add a #if that reports LP64 when LONG_MAX > INT_MAX and ILP32 otherwise. Finish with a static_assert requiring CHAR_BIT to be 8 and change it to 9 to see the build fail.
Open the C workspaceCheck your understanding
With <limits.h> included, #if LONG_MAX > 2147483647 compiles fine but #if sizeof(long) == 8 does not. Why?
- sizeof yields size_t, which is unsigned, and #if cannot evaluate unsigned expressions.
- sizeof is only valid inside a function body, so it cannot appear at file scope.
- #if is evaluated during preprocessing, where no types exist; LONG_MAX expands to an ordinary integer literal the preprocessor can compare.
- Both forms are legal, but sizeof(long) == 8 is false where long is 4 bytes, so that branch is simply skipped.
Show answer
A #if condition is evaluated on preprocessing tokens, long before the compiler knows what a type is, so the tokens sizeof and long carry no meaning there and the line is a hard syntax error. Option 4 is tempting because most failed conditions do just skip a branch, but here the file never compiles; and option 1 is wrong because the preprocessor does perform unsigned arithmetic — the problem is the absence of type information, not signedness.