[KongchangAI]
· 2 min read· 1,451 words

Your C Isn't Standard C? A Deep Dive into the Reality Gap in the C Standard

Your C Isn't Standard C? A Deep Dive into the Reality Gap in the C Standard

The ISO C standard and the C code actually written have a gap, mainly due to compiler extensions and undefined behavior.

The article starts from the "your C is not standard C" discussion on Hacker News and reveals the widespread gap between everyday C code and the ISO/IEC 9899 standard. It first explains the definition of standard C and the evolution of its versions, then analyzes three major causes of the gap: GCC/Clang extensions (such as __attribute__, typeof, statement expressions, and zero-length arrays) are widely relied upon in systems programming but are not part of the standard; undefined behavior leads developers to mistake the actual behavior of a specific compiler for standard behavior; and differences in standard library implementations across platforms make "portable" often mean only "compiles." The article then contrasts the two positions of purism and pragmatism, and proposes a pragmatic approach: understand the extensions you use, isolate them in platform-specific layers, and provide standard fallbacks. Finally, it points out that understanding this tension helps write more robust code, and that the vitality of the C language stems precisely from the ongoing contest between standard and implementation.

In programming communities, the phrase "your C is not standard C" frequently sparks heated debate. A Hacker News thread (with a link pointing to the discussion page) touches on an awkward reality that C developers have faced for a long time: the C code we write day to day often diverges significantly from the "standard C" defined by the ISO C standard.

rss source: your C is not standard C

What Is "Standard C"?

The C language standard is defined by the ISO/IEC 9899 specification, and has gone through multiple versions including C89, C99, C11, C17, and C23. The standard text lays out in detail the language's syntax, semantics, standard library behavior, and the minimum requirements that implementations must satisfy. In theory, as long as code conforms to the standard, it should compile and run on any conforming compiler.

In reality, however, almost no developer can write C completely free of compiler extensions, platform-specific behavior, and undefined behavior (UB). Even projects renowned for portability, such as the Linux kernel and SQLite, rely heavily on GCC/Clang extensions.

Why Your C Isn't Standard C

Compiler Extensions Are Everywhere

GCC and Clang provide a large number of extensions beyond the standard, such as __attribute__, statement expressions, typeof, zero-length arrays, and more. These extensions are extremely common in systems programming, but strictly speaking they are not part of standard C. When code is migrated to MSVC or other compilers, these usages will cause errors.

So-called compiler extensions refer to syntax or semantic features that compilers implement in addition to the ISO C standard. They are not written into the ISO/IEC 9899 specification, so strictly speaking they are not portable C code. The extension systems of GCC and Clang are the most extensive: __attribute__((...)) can attach metadata such as alignment, packing, deprecation warnings, and inline hints to functions, variables, and types; statement expressions ({ ... }) allow a compound statement to be written in expression position and return the value of its last expression; typeof can deduce an expression's type at compile time; and zero-length arrays are widely used as a precursor to flexible array members.

These extensions are popular because standard C lacks the corresponding expressive power: standard flexible array members were not introduced until C99, while control mechanisms for alignment, inlining, and branch prediction either do not exist in the standard or have verbose syntax. MSVC takes a different route, implementing the __declspec family of extensions and some C11/C17 features, but mostly not supporting GCC-style extensions. This is precisely the root cause of frequent errors during cross-compiler migration.

The Gray Zone of Undefined Behavior

Many behaviors in the C standard are defined as "undefined," such as signed integer overflow, out-of-bounds access, and sequence point violations. The standard allows compilers to handle these situations arbitrarily. Developers often rely on the actual behavior of a particular compiler and mistakenly assume that is "standard."

Undefined Behavior (UB) in the ISO C standard is stricter than the name suggests: the standard not only fails to specify the result, but actually allows an implementation to do anything when it encounters such behavior, including producing seemingly correct output, crashing, silently corrupting data, or deleting the relevant code entirely after optimization. Typical scenarios explicitly listed as UB in the standard include signed integer overflow, out-of-bounds array reads and writes, dereferencing null or dangling pointers, violating sequencing rules (such as i = i++ + 1;), and reading uninitialized variables.

The key point is that UB is not "a problem that only triggers at runtime," but rather a premise that the compiler can use for reasoning during the optimization phase. For example, the compiler assumes signed addition never overflows, thereby deducing that certain boundary conditions are unreachable and directly deleting the corresponding check branches—this is extremely common at -O2 and above. Therefore, "my code ran fine on GCC" does not prove that it has no UB; it merely means that under the current compiler version and optimization level it happens to exhibit the expected behavior. Common workarounds developers use include enabling -fsanitize=undefined, -Wall -Wextra, and testing under multiple compilers, but the fundamental approach is still to understand and avoid triggering UB.

Differences in Standard Library Implementations

Even for standard library functions, implementations differ across platforms. For example, the formatting behavior of printf, the stability of qsort, and thread-safety guarantees—the standard often only gives minimum requirements.

Standard library implementations refer to the concrete code each platform provides for the set of library functions specified by ISO C. The standard only specifies the behavioral contract and minimum requirements of the functions, leaving implementation details to the specific library, such as glibc, musl, macOS's libSystem, and Windows's UCRT and MSVCRT. Differences appear at multiple levels: printf's rounding and formatting of %f may differ at extreme precision; qsort is only required to sort correctly, with neither stability guaranteed nor algorithm specified (glibc uses merge sort to ensure stability, while other implementations may use quicksort); the sequence and range of values produced by rand are entirely implementation-defined, so they cannot be reproduced across platforms; functions such as strtok and localtime are not thread-safe, while POSIX provides separate _r versions. In addition, edge cases such as malloc(0), overlapping source and destination in memcpy, and a qsort comparison function triggering recursion are either defined as UB by the standard or left to the implementation. This means that even if you use only standard headers and standard functions, cross-platform behavior may still be inconsistent, and so-called "portable" often just means "compiles" rather than "produces the same results when run."

Pragmatism vs. Pure Standard

The discussion on Hacker News reflects two positions. Purists argue that adhering to standard C brings maximum portability and reduces technical debt. Pragmatists point out that completely avoiding extensions in real projects is almost impossible, and that some extensions can significantly improve performance or expressiveness.

A pragmatic attitude might be: understand the extensions you are using, isolate them in platform-specific layers, and provide standard fallbacks when necessary. In this way, you can enjoy the convenience brought by extensions while preserving room for portability.

Implications for Developers

Understanding the gap between "standard C" and "actual C" helps you write more robust code. When you use compiler extensions, know exactly what you are doing; when code needs to be cross-platform, identify potential non-standard usage in advance. The vitality of the C language lies precisely in this tension between standard and implementation.

Background Supplement

ISO/IEC 9899 is the official number of the C language standard, maintained by the ISO/IEC JTC1/SC22/WG14 working group. The major published versions include C89 (later lightly revised into C90, i.e., ANSI X3.159-1989), C99 (which introduced // line comments, variable-length arrays, long long, complex numbers, stdint.h, and inline), C11 (which introduced _Generic, _Static_assert, thread and atomic operation headers, and anonymous structs), C17 (which mainly corrected defects in C11 and had no major language features), and C23 (which added nullptr, typeof, constexpr, binary literals, attribute syntax [[...]], #embed, and more). In addition to the ISO version, there is an ANSI version, and the two are basically identical in content.

It should be noted that the standard divides clauses into "mandatory requirements" and "recommended practices," and allows implementations to provide extensions as long as they do not change the behavior of strictly conforming programs. The language also distinguishes among "strictly conforming program," "conforming program," and "conforming implementation." What many developers call "standards-compliant" is actually the weaker of these three concepts, and this is also the semantic basis that makes the claim "your C is not standard C" tenable.

One-line Summary

The ISO C standard and the C code actually written have a gap, mainly due to compiler extensions and undefined behavior.

Paragraph Summary

The article starts from the "your C is not standard C" discussion on Hacker News and reveals the widespread gap between everyday C code and the ISO/IEC 9899 standard. It first explains the definition of standard C and the evolution of its versions, then analyzes three major causes of the gap: GCC/Clang extensions (such as attribute, typeof, statement expressions, and zero-length arrays) are widely relied upon in systems programming but are not part of the standard; undefined behavior leads developers to mistake the actual behavior of a specific compiler for standard behavior; and differences in standard library implementations across platforms make "portable" often mean only "compiles." The article then contrasts the two positions of purism and pragmatism, and proposes a pragmatic approach: understand the extensions you use, isolate them in platform-specific layers, and provide standard fallbacks. Finally, it points out that understanding this tension helps write more robust code, and that the vitality of the C language stems precisely from the ongoing contest between standard and implementation.

#C standard#ISO C#compiler extensions#undefined behavior#portability
Share:

Related articles