Skip to content

Undefined, Unspecified, and Implementation-Defined Behavior

C gives implementations freedom so the language can map efficiently across hardware and operating systems. That freedom is classified. Engineering safely begins by naming the category instead of treating every surprising result as an ordinary platform difference.

Undefined behavior provides no portable outcome

Out-of-bounds access, use after free, signed integer overflow, invalid shifts, data races, and many lifetime or aliasing violations produce undefined behavior. The standard imposes no requirements on the execution.

c
int values[3] = {1, 2, 3};
/* int value = values[3];  undefined: one-past is not an element */

The program may appear to work, crash, leak data, or behave differently after optimization. A debugger observation does not define a contract.

Compilers optimize under the assumption that a valid program does not execute undefined operations. For signed integers:

c
if (value + 1 > value) {
    /* optimizer may assume true when the addition is defined */
}

If value is the maximum int, the addition has already left the defined program domain. The compiler need not preserve wraparound intuition.

Unspecified behavior chooses among permitted outcomes

When behavior is unspecified, the standard permits a set of possibilities and the implementation need not document which occurs on each occasion. Function argument evaluation order is a common example.

Do not write logic whose correctness depends on one choice:

c
int first = next_value();
int second = next_value();
consume(first, second);

Separate statements establish the intended order.

Implementation-defined behavior must be documented

The implementation chooses and documents a behavior. Examples include the size of many integer types, whether plain char is signed, and behavior of some out-of-range signed conversions.

Implementation-defined does not mean erroneous. It means a portable project either avoids relying on the choice, checks it with macros/static assertions, or declares a supported platform contract.

Locale-specific and indeterminate cases add nuance

The standards also use categories such as locale-specific behavior and indeterminate values. The vocabulary matters during precise analysis, but the everyday policy remains: do not read an indeterminate value; define locale at input/output boundaries; do not infer a universal result from one execution.

Diagnostics have limits

An implementation must diagnose certain constraint violations, but it is not required to prove the absence of undefined behavior. Many bugs depend on runtime paths and values.

Use overlapping defenses:

  • strong compiler warnings;
  • AddressSanitizer and UndefinedBehaviorSanitizer in test builds;
  • ThreadSanitizer for supported concurrency tests;
  • static analysis;
  • boundary validation and checked size arithmetic;
  • tests across optimized and unoptimized builds;
  • more than one compiler for portable code.

Sanitizer success proves only that instrumented executions did not trigger the checks available in that configuration. It is powerful evidence, not formal verification.

Define a project portability contract

Absolute portability to every conforming implementation is rarely the real requirement. A project can deliberately require eight-bit bytes, two's-complement integers, IEEE-style floating point, POSIX, or a particular ABI—provided it checks and documents those assumptions.

c
#include <limits.h>

_Static_assert(CHAR_BIT == 8, "this format requires 8-bit bytes");

Use fixed-width types and explicit endian conversion for serialized formats. Avoid relying on struct padding, enum width, bit-field order, or host pointer size.

Defensive rules should connect to failure modes

“Never use pointers” is not actionable in C. Better rules identify the proof obligation:

  • Index only within a known extent.
  • Check allocation-size arithmetic before allocating.
  • Initialize objects before reads.
  • Keep ownership transfer explicit.
  • Ensure shifts use valid counts and representable operands.
  • Avoid mixed signedness unless conversion is intentional.
  • Do not retain pointers past object lifetime or reallocation.
  • Synchronize conflicting cross-thread accesses.

These rules explain why a pattern is dangerous and how to construct a valid alternative.

Optional prompts

Explain: Why can optimization change the visible result of a program with signed overflow?

Answer: Signed overflow is undefined. Optimizers reason about executions in which defined operations remain in range, so an observation from an unoptimized invalid execution is not a contract they must preserve.

Classify: The implementation documents that plain char is signed. Is that undefined or implementation-defined?

Answer: Implementation-defined. The standard permits a choice and requires the implementation to document it.

Further reference