Skip to content
VLSI Mentor

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

Three categories of non-portable behaviour compared on three questions. Unspecified behaviour: the standard gives two or more possibilities, the implementation need not document which it picks, and it may pick differently at different occurrences, so you may not rely on it even after naming your compiler. Implementation-defined behaviour: the standard gives two or more possibilities, the implementation must document its choice, and the choice is stable, so you may rely on it once you have named your compiler and target. Undefined behaviour: the standard imposes no requirements at all, nothing is documented, and you may never rely on it.CategoryChoice from aset?Must bedocumented?May you rely onit?after naming compiler+ targetUnspecifiedargument evaluationorderyestwo or morepossibilitiesnonomay differ peroccurrenceImplementation-definedsizeof(int) ·signedness of charyesyesthe implementationmust sayYESthis is the one youmay useUndefinednull dereference ·signed overflownono requirements at allnonevernot even on onemachine12
Figure 1 — the three categories against the three questions that matter. The first two columns are nearly identical for the top two rows, which is why they get conflated; the whole distinction lives in the third column of the first two rows. Read the middle row as the chapter's main point: implementation-defined behaviour is something you may use, deliberately and permanently, once you have said which compiler and which target you are building for. The top row cannot be used that way even though it also offers a choice from a known set, because nothing promises the choice is the same twice.

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.

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
    takes3(mark("first-written"), mark("second-written"), mark("third-written"));
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
--- 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 3rd

Left 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.

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
    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.

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
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                = 2147483647

Each of those is a decision the standard explicitly delegates:

BehaviourWhat the standard says
the size of intat least 16 bits; the actual width is the implementation's
the signedness of plain charmay be signed or unsigned — a separate type from both
>> on a negative valueimplementation-defined whether it is arithmetic or logical
how a negative value converts to an unsigned typedefined by the standard; the reverse is implementation-defined
the representation of a null pointer constant in memoryimplementation-defined
what malloc(0) returnsimplementation-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:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
    #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 likeUsually
a question about sizes, widths, ranges, signedness, representationsimplementation-defined
a question about order, or which of several equivalent thingsunspecified
a question about validity — of a pointer, an index, a lifetime, an arithmetic resultundefined

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.

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
    /* 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

  1. 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.
  2. Implementation-defined is literally a subset of unspecified — the only difference is that the choice must be written down.
  3. The decisive phrase is "in any instance": unspecified behaviour may differ between two occurrences in one program.
  4. So naming your compiler does not make unspecified behaviour usable, and does make implementation-defined behaviour usable.
  5. Measured: argument evaluation was left to right at -O0 and -O2 — one permitted choice, not a rule, and a different order could not be demonstrated with one compiler.
  6. Never write an expression whose result depends on evaluation order — split it into statements.
  7. f(i++, i++) is undefined; f(g(), h()) with side effects is unspecified. Different categories.
  8. Measured implementation-defined facts: sizeof(int) 4, CHAR_BIT 8, plain char signed (CHAR_MIN −128), -8 >> 1 is −4 (arithmetic).
  9. <limits.h> is the documentation obligation in machine-readable form.
  10. 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.
  11. Part II depends on exactly this — fixed-width types, pointer casts, memory-mapped registers.
  12. 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?

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
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 int

Exercise 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.

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
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
  • char is signed
  • (unsigned)-1 == UINT_MAX
  • *(int *)0 traps

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_BIT 8 · char signed · -8 >> 1 = −4.
  • <limits.h> is the documentation obligation, machine-readable.
  • Rely on implementation-defined behaviour deliberately: write it down · _Static_assert it · 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.