C Part I · Module 13 · Writing Correct and Reliable C
Unspecified and Implementation-Defined Behavior
The previous chapter said repeatedly that undefined behaviour is the strongest of three categories, without explaining the other two. That evasion has to end here, because the distinction is where a great deal of practical C actually lives. Some things the standard declines to define are dangerous in exactly the way just described. Others it merely declines to choose — leaving the implementation free to pick, and in one case requiring it to write the choice down. That requirement, a single documentation obligation, is the entire difference between a construct you must never depend on and one you may depend on completely. Most programmers treat all three as equally forbidden, which is a mistake in both directions: it makes them careless about the dangerous one and needlessly afraid of the useful one.
Advanced19 min readCPart IFundamentalsCorrectness
Part I · Module 13 · Chapter 13.3 · Unspecified and Implementation-Defined Behavior
1. The Definitions
All three are in C17 §3.4, and they are short enough to quote in full.
§3.4.4 unspecified behavior — behavior, upon use of an unspecified value, or other behavior where this International Standard provides two or more possibilities and imposes no further requirements on which is chosen in any instance.
§3.4.1 implementation-defined behavior — unspecified behavior where each implementation documents how the choice is made.
§3.4.3 undefined behavior — behavior ... for which this International Standard imposes no requirements.
Read §3.4.1 literally: implementation-defined behaviour is a subset of unspecified behaviour, distinguished by one thing — the implementation has to write the choice down. That sounds like a formality and is the entire practical difference, because a documented choice is one you can look up and depend on, and an undocumented one is not.
There is a fourth, which c.ts does not count among the three and which is worth naming for accuracy: §3.4.2 locale-specific behaviour — behaviour that depends on local conventions for things like character classification and decimal points. It behaves like implementation-defined for practical purposes.
2. The Difference That Decides Everything
The phrase that separates the top two rows is "in any instance". Unspecified behaviour is not a choice the implementation makes once; it is a choice it may make separately at every occurrence. Two identical-looking expressions in one program may be resolved differently, and nothing is violated.
That is why discovering the answer does not help. You can observe that your compiler evaluates arguments left to right, and the observation tells you nothing about the next call site, let alone the next build.
Implementation-defined behaviour is the opposite: the choice is made by the implementation, written down, and stable. Once you have named your compiler and your target, it is a fact about your program.
3. Unspecified: Argument Evaluation Order
The canonical example. In f(g(), h()), the standard does not say whether g or h runs first.
takes3(mark("first-written"), mark("second-written"), mark("third-written"));--- compiled at -O0 ---
first-written evaluated 1st
second-written evaluated 2nd
third-written evaluated 3rd
--- compiled at -O2 ---
first-written evaluated 1st
second-written evaluated 2nd
third-written evaluated 3rdLeft to right, at both optimisation levels.
That is one permitted answer and nothing more. Nothing requires this compiler to choose it, nothing requires it to document the choice, and nothing requires the next call site in the same file to match. A reader who takes the measurement as a rule has learned the opposite of the lesson.
An honest limit on this measurement: only one compiler was available here, so a different order could not be demonstrated. Historically, several widely used compilers evaluated right to left, and code that assumed left to right broke when moved.
The practical rule follows directly:
Never write an expression whose result depends on evaluation order. Not because it is undefined — it is not, each operand is evaluated exactly once and the program is well-formed — but because the result is one you cannot predict, cannot look up, and cannot pin down by testing.
log_pair(next_id(), next_id()); /* which id goes to which parameter? */
int n = read_count() + read_count(); /* order of the two reads: unspecified */
/* say what you mean instead */
int a = next_id();
int b = next_id();
log_pair(a, b);Other common unspecified cases you have already met: the values of padding bytes in a structure (Chapter 10.5 §7 measured two objects with equal members and different bytes), whether two identical string literals share storage (Chapter 8.2), and the order in which subexpressions of most operators are evaluated.
One distinction to keep clear, because the two are easy to confuse. Unspecified evaluation order is not the same as the undefined behaviour of modifying an object twice without a sequence point. f(i++, i++) is undefined; f(g(), h()) where both have side effects is unspecified. The first has no meaning at all; the second has one of two meanings and you do not get to know which.
4. Implementation-Defined: The Useful Category
These are the choices your compiler had to make and had to write down.
sizeof(int) = 4
CHAR_BIT = 8
plain char is SIGNED (CHAR_MIN = -128)
right shift of -8 by 1 = -4 (arithmetic, not logical)
INT_MAX = 2147483647Each of those is a decision the standard explicitly delegates:
| Behaviour | What the standard says |
|---|---|
the size of int | at least 16 bits; the actual width is the implementation's |
the signedness of plain char | may be signed or unsigned — a separate type from both |
>> on a negative value | implementation-defined whether it is arithmetic or logical |
| how a negative value converts to an unsigned type | defined by the standard; the reverse is implementation-defined |
| the representation of a null pointer constant in memory | implementation-defined |
what malloc(0) returns | implementation-defined — Chapter 9.6 §6 |
And the documentation obligation is discharged in a form you can use. <limits.h> is not a list of trivia; it is the standard requiring implementations to publish these answers in machine-readable form:
#include <limits.h>
#if CHAR_MIN < 0
/* plain char is signed here */
#endif
printf("this build: int is %zu bytes, char is %s\n",
sizeof(int), CHAR_MIN < 0 ? "signed" : "unsigned");5. Undefined: The One With No Floor
Chapter 13.2 is the full treatment. Collected here only for the contrast:
There is no set of possibilities. The other two categories promise "one of these"; undefined behaviour promises nothing, which is what lets a compiler delete code on the assumption that it never happens — measured in Chapter 13.2 §3, where a null check written after a dereference was removed from the program entirely.
There is no documentation obligation, and in general no documentation is possible: the behaviour depends on optimisation decisions that vary with flags, inlining and compiler version.
And naming your platform does not help. Chapter 13.2 §5 measured the same source producing a 17-instruction function and a 2-instruction one depending on one flag. There is nothing stable to name.
Implementation-defined behaviour is a choice you can look up. Undefined behaviour is the absence of a question.
6. Telling Them Apart
Three practical approaches, in order of reliability.
Check Annex J. C17's Annex J lists, in separate sections, every unspecified behaviour (J.1), every implementation-defined behaviour (J.3) and every undefined behaviour (J.2). It is the definitive answer and it is a list rather than prose, so looking something up is fast.
Read the wording where the behaviour is specified. The standard's main text uses the exact phrases — "the behavior is undefined", "it is implementation-defined whether", "the order ... is unspecified" — so the category is almost always stated in the sentence that describes the construct.
Apply the question from Figure 1. If the standard offers a set of possibilities and requires documentation, it is implementation-defined; a set without documentation is unspecified; no set at all is undefined.
And a rough but useful heuristic for code you are reading, when you have no reference to hand:
| Smells like | Usually |
|---|---|
| a question about sizes, widths, ranges, signedness, representations | implementation-defined |
| a question about order, or which of several equivalent things | unspecified |
| a question about validity — of a pointer, an index, a lifetime, an arithmetic result | undefined |
That third row is the one to internalise: undefined behaviour is nearly always about doing something to an object you were not entitled to do it to.
7. Writing Code That Says What It Assumes
Collecting the practice this chapter argues for.
Undefined: never. No assumption makes it safe, no platform makes it stable. Chapter 13.2 §7.
Unspecified: avoid depending on it, and restructure rather than hope. Splitting f(g(), h()) into two statements costs one line and removes the question. Where the behaviour is unavoidable — padding bytes exist whether you like it or not — the rule is not to read meaning from it: compare structures member by member (Chapter 10.5 §7), never byte by byte.
Implementation-defined: use it, and say so.
/* This module assumes:
- CHAR_BIT == 8
- int is 32 bits, two's complement
- plain char is signed
It is built for clang/gcc on arm64 and x86-64. */
_Static_assert(CHAR_BIT == 8, "8-bit bytes assumed");
_Static_assert(sizeof(int) == 4, "32-bit int assumed");Three properties of that block are worth copying: it is specific about which assumptions, it is checked so a violating platform fails at build time rather than at run time, and it names the targets so a reader knows what was tested.
_Static_assert is C11 and available in C17; C23 spells it static_assert without a header. It is the mechanism Chapter 13.8 covers properly, and this is its best use — an assumption about the platform, checked once, at compile time, costing nothing at run time.
8. What You Should Be Able to Say Now
- C17 §3.4 defines unspecified (two or more possibilities, no requirement on which), implementation-defined (unspecified plus a documentation obligation), and undefined (no requirements at all). A fourth, locale-specific, also exists.
- Implementation-defined is literally a subset of unspecified — the only difference is that the choice must be written down.
- The decisive phrase is "in any instance": unspecified behaviour may differ between two occurrences in one program.
- So naming your compiler does not make unspecified behaviour usable, and does make implementation-defined behaviour usable.
- Measured: argument evaluation was left to right at
-O0and-O2— one permitted choice, not a rule, and a different order could not be demonstrated with one compiler. - Never write an expression whose result depends on evaluation order — split it into statements.
f(i++, i++)is undefined;f(g(), h())with side effects is unspecified. Different categories.- Measured implementation-defined facts:
sizeof(int)4,CHAR_BIT8, plaincharsigned (CHAR_MIN−128),-8 >> 1is −4 (arithmetic). <limits.h>is the documentation obligation in machine-readable form.- You may rely on implementation-defined behaviour once you write it down, check it with
_Static_assert, confine it, and revisit it on a new target. - Part II depends on exactly this — fixed-width types, pointer casts, memory-mapped registers.
- Annex J lists all three categories separately; and the heuristic: sizes → implementation-defined, order → unspecified, validity → undefined.
9. Exercises
Exercise 1 — classify each. Unspecified, implementation-defined, or undefined?
sizeof(long)
the order of evaluation of f() and g() in h(f(), g())
*p where p is NULL
whether plain char is signed
INT_MAX + 1
the value of a structure's padding bytes
-8 >> 1
reading an uninitialised local whose address was never taken
whether "abc" and "abc" share storage
converting -1 to unsigned intExercise 2 — fix the dependence. Rewrite each so the result does not depend on evaluation order, and say what the original would have been worth if the order had been documented.
printf("%d %d\n", next(), next());
total = read() - read();
set_range(min_of(a, b), max_of(a, b));Exercise 3 — may you rely on it. For each, say yes or no and why, assuming you have named clang on arm64.
sizeof(int) == 4- arguments are evaluated left to right
charis signed(unsigned)-1 == UINT_MAX*(int *)0traps
Exercise 4 — write the assumption block. A module requires 8-bit bytes, a 32-bit int, and unsigned char for byte access. Write the comment and the _Static_assert lines, and say what each assertion prevents.
Exercise 5 — explain "in any instance". In three sentences, explain why observing your compiler's argument order does not let you rely on it, and why observing sizeof(int) does.
Exercise 6 — two categories, one expression. Explain why f(i++, i++) and f(g(), h()) fall into different categories, and what the practical difference is for a programmer reading each.
Exercise 7 — the Part II case. A colleague objects that Chapter 14.4's integer-to-pointer conversion is "non-portable and therefore wrong". Give the three-sentence reply, naming the category and the conditions under which it is legitimate.
Exercise 8 — audit a header. A header defines typedef int int32; with no comment or check. List everything wrong with it and rewrite it.
10. Summary
- Unspecified: two or more possibilities, no documentation, may differ per occurrence.
- Implementation-defined: unspecified plus the implementation must document its choice — literally a subset.
- Undefined: no requirements at all (13.2).
- The decisive phrase is "in any instance" — which is why naming your compiler rescues one category and not the other.
- Measured: argument order left to right here — one permitted choice, not a rule. Never depend on evaluation order.
- Measured implementation-defined:
sizeof(int)4 ·CHAR_BIT8 ·charsigned ·-8 >> 1= −4. <limits.h>is the documentation obligation, machine-readable.- Rely on implementation-defined behaviour deliberately: write it down ·
_Static_assertit · confine it · revisit on a new target. - Part II is built on this. The error is not using it — it is using it silently.
- Annex J is the catalogue; sizes → implementation-defined, order → unspecified, validity → undefined.
Two of the categories above were illustrated with the same example, and it is the one most likely to be in your code already. The signedness of char, the width of int, the behaviour of >> on a negative value — all implementation-defined, all fine once named. But arithmetic has a second way of going wrong that is not in that category at all: a calculation on perfectly ordinary ints, with no pointers and no memory involved, whose result does not fit. For unsigned types the standard says exactly what happens and the answer is well-defined and occasionally surprising. For signed types it says nothing whatsoever, which puts an everyday a + b squarely in Chapter 13.2's territory — and makes the compiler's assumption that it never overflows into something that reshapes your loops. The next chapter is that asymmetry and how to write arithmetic that stays inside it.
Standards & specifications
- Governing standard
- ISO/IEC 9899 (the C language standard)(opens ISO/IEC (JTC 1/SC 22/WG 14) in a new tab)
Defines the C language itself — the type system, expression semantics, object model, storage duration and the behaviour categories a conforming implementation must honour. It is a FAMILY of editions (C89/C90, C99, C11, C17, C23) and this curriculum names the edition wherever a claim depends on one. It does not define any foreign-function interface to a hardware description language, and it does not define platform behaviour: what a memory-mapped access does on real silicon is the implementation's and the device's contract, not this standard's. Working drafts published by WG14 are freely available and are what most engineers read day to day; a draft is not the published standard.
This page also covers RTL structure, verification approach and debugging technique. Those are engineering practice built on the standard, not requirements the standard itself imposes.
Where this fits
Part of the C Programming curriculum.
