ZetCode

C compilers: GCC and Clang

last modified November 14, 2026

GCC and Clang are the two C compilers that matter on Linux and macOS. Both accept the -std option, both implement the same ISO standards, and both can build the same programs. They do not, however, implement the newest standards at the same time or to the same depth. This tutorial compares the two compilers feature by feature and answers one question: which of them supports the new C standards better? The short answer is GCC for the published C23 standard; the measurements, the remaining gaps, and the exceptions follow below.

Two compilers, one language

GCC is the GNU Compiler Collection. Its C compiler is the cc1 front end, which the gcc driver runs before the assembler and the linker. Clang is the C, C++ and Objective-C front end of the LLVM project, and the clang driver does the same four steps: preprocess, compile, assemble and link. Both emit ordinary object files and both link against the same C library, which on this machine is glibc. That split matters when a standard is evaluated: the compiler owns the language, while the C library owns the headers and functions.

The language is selected with -std=. The strict names are c17, c23 and, for the draft that follows C23, c2y; the gnu17, gnu23 and gnu2y names add GNU extensions. A program compiled without any -std option uses the compiler's default dialect, and the default is one of the biggest practical differences between the two compilers.

The examples below were run on GCC 13.2.0 and Clang 18.1.8, the versions installed here. The current releases at the time of writing are GCC 16.2 and Clang 23.1.5, and the section on current releases uses their official status pages for the newer features that the installed compilers do not have. Note that cc is not necessarily either of them: on this system the cc command runs GCC 11.4, so the commands below name gcc and clang explicitly.

The default C standard

The __STDC_VERSION__ macro reports the dialect in use: 201710L for C17 and 202311L for C23. The following program prints it.

standards.c
#include <stdio.h>

int main(void) {
#ifdef __STDC_VERSION__
    printf("__STDC_VERSION__ = %ldL\n", __STDC_VERSION__);
#else
    printf("__STDC_VERSION__ is not defined\n");
#endif
    return 0;
}

Compiled with no options, both compilers report 201710L: the default is C17 in each of these versions. GCC 13 needs the older spelling -std=c2x, because the -std=c23 alias arrived in GCC 14; in that mode it reports 202000L, the placeholder value used before ISO published C23. Clang 18 understands -std=c23 and reports the final 202311L. Current GCC, from version 15, defaults to gnu23, while Clang still defaults to gnu17.

$ gcc standards.c -o standards
$ ./standards
__STDC_VERSION__ = 201710L
$ clang standards.c -o standards
$ ./standards
__STDC_VERSION__ = 201710L
$ gcc -std=c2x standards.c -o standards
$ ./standards
__STDC_VERSION__ = 202000L
$ gcc -std=c23 standards.c -o standards
gcc: error: unrecognized command-line option ‘-std=c23’; did you mean ‘-std=c2x’?
$ clang -std=c23 standards.c -o standards
$ ./standards
__STDC_VERSION__ = 202311L

C23 keywords

The most visible C23 change is that bool, true, false, nullptr, static_assert, alignas, alignof and thread_local are keywords. In C17 they are macros or typedefs supplied by <stdbool.h>, <stddef.h>, <assert.h>, <stdalign.h> and <threads.h>. This program uses the names without including any of those headers.

keywords.c
#include <stdio.h>

int main(void) {
    bool flag = true;
    static_assert(sizeof(int) >= 2);
    int *p = nullptr;

    printf("flag = %d\n", flag);
    printf("p is null: %d\n", p == nullptr);
    return 0;
}

Both installed compilers accept the file in C23 mode and print the same result. Clang has supported the keyword spellings since Clang 17, and GCC since GCC 13. The one-argument form of static_assert is also new: C11 required a message string, C23 makes it optional.

$ gcc -std=c2x keywords.c -o keywords
$ ./keywords
flag = 1
p is null: 1
$ clang -std=c23 keywords.c -o keywords
$ ./keywords
flag = 1
p is null: 1

typeof, constexpr and auto

C23 added three declarations that used to be compiler extensions or C++ only: typeof and typeof_unqual for the type of an expression, constexpr for compile-time constants, and auto for type inference. The next example uses all three.

inference.c
#include <stdio.h>

int main(void) {
    constexpr int size = 4;
    typeof(size) n = size;
    auto ratio = 2.5;

    printf("n = %d\n", n);
    printf("ratio = %g\n", ratio);
    return 0;
}

GCC 13 implements all three and builds the file. Clang 18 understands typeof (Clang 16) and auto (Clang 18) but not constexpr, which arrived in Clang 19, so the declaration fails and the two uses of size fail with it. Removing the constexpr line makes Clang 18 accept the rest.

$ gcc -std=c2x inference.c -o inference
$ ./inference
n = 4
ratio = 2.5
$ clang -std=c23 inference.c -o inference
inference.c:4:5: error: use of undeclared identifier 'constexpr'
    4 |     constexpr int size = 4;
      |     ^
inference.c:5:12: error: use of undeclared identifier 'size'
    5 |     typeof(size) n = size;
      |            ^
inference.c:5:22: error: use of undeclared identifier 'size'
    5 |     typeof(size) n = size;
      |                      ^
3 errors generated.

Bit-precise integers

_BitInt(N) declares an integer with exactly N bits, including widths that are not a power of two. The next program stores 100 in an unsigned seven-bit integer.

bitint.c
#include <stdio.h>

int main(void) {
    unsigned _BitInt(7) x = 100;

    printf("x = %u\n", (unsigned) x);
    return 0;
}

The installed compilers disagree in the opposite direction this time: GCC 13 predates the feature and fails to parse the declaration, while Clang has supported _BitInt since Clang 15 and builds and runs the program. GCC added it in GCC 14, and only for IA-32, x86-64 and little-endian AArch64.

$ gcc -std=c2x bitint.c -o bitint
bitint.c: In function ‘main’:
bitint.c:4:22: error: expected declaration specifiers or ‘...’ before numeric constant
    4 |     unsigned _BitInt(7) x = 100;
      |                      ^
bitint.c:6:35: error: ‘x’ undeclared (first use in this function)
    6 |     printf("x = %u\n", (unsigned) x);
      |                                   ^
bitint.c:6:35: note: each undeclared identifier is reported only once for each function it appears in
$ clang -std=c23 bitint.c -o bitint
$ ./bitint
x = 100

Embedding files

The #embed directive turns a file into an initializer at compile time, without an external converter such as xxd. Here a five-byte file holds the word Hello, and the program embeds it in a byte array.

embed.c
#include <stdio.h>

int main(void) {
    static const unsigned char data[] = {
#embed "data.bin"
    };

    printf("%zu bytes, first byte %d\n", sizeof data, data[0]);
    return 0;
}

Both installed compilers reject the directive, and for the same reason: the feature is newer than they are. GCC implements #embed in GCC 15 and Clang in Clang 19. The diagnostic wording differs, which is typical of the two front ends.

$ printf 'Hello' > data.bin
$ gcc -std=c2x embed.c -o embed
embed.c: In function ‘main’:
embed.c:5:2: error: invalid preprocessing directive #embed
    5 | #embed "data.bin"
      |  ^~~~~
$ clang -std=c23 embed.c -o embed
embed.c:5:2: error: invalid preprocessing directive
    5 | #embed "data.bin"
      |  ^
1 error generated.

Attributes from C23

C23 attributes use the double-bracket syntax of C++. [[unsequenced]] and [[reproducible]] describe functions without side effects and without dependence on global state. The next program marks a pure function and calls it.

attrs.c
#include <stdio.h>

[[unsequenced]] int square(int x) { return x * x; }

int main(void) {
    printf("square(7) = %d\n", square(7));
    return 0;
}

Neither installed version knows the attribute, but unknown attributes are only a warning by default, so both programs still build and print 49. GCC 13 says the attribute was ignored; Clang 18 calls it unknown. With -Werror both stop with an error, and GCC 15 is the first GCC that implements the attribute. Clang's status page still lists unsequenced functions as not implemented, so even Clang 23 warns about this file.

$ gcc -std=c2x -Wall attrs.c -o attrs
$ ./attrs
attrs.c:3:1: warning: ‘unsequenced’ attribute ignored [-Wattributes]
    3 | [[unsequenced]] int square(int x) { return x * x; }
      | ^
square(7) = 49
$ clang -std=c23 -Wall attrs.c -o attrs
$ ./attrs
attrs.c:3:3: warning: unknown attribute 'unsequenced' ignored [-Wunknown-attributes]
    3 | [[unsequenced]] int square(int x) { return x * x; }
      |   ^~~~~~~~~~~
1 warning generated.
square(7) = 49

Feature test macros

A portable program asks the preprocessor what the compiler supports instead of guessing from its name. __has_c_attribute tests an attribute and __STDC_VERSION__ tests the dialect. The version is an ordinary macro, but __has_c_attribute is an operator that may only appear in a preprocessing directive, so both results are stored in macros.

attributes.c
#include <stdio.h>

#if defined(__has_c_attribute)
#  if __has_c_attribute(unsequenced)
#    define HAVE_UNSEQUENCED 1
#  else
#    define HAVE_UNSEQUENCED 0
#  endif
#  define HAVE_ATTRIBUTE_QUERY 1
#else
#  define HAVE_UNSEQUENCED 0
#  define HAVE_ATTRIBUTE_QUERY 0
#endif

int main(void) {
    printf("__STDC_VERSION__ = %ldL\n", __STDC_VERSION__);
    printf("__has_c_attribute = %d\n", HAVE_ATTRIBUTE_QUERY);
    printf("[[unsequenced]] = %d\n", HAVE_UNSEQUENCED);
    return 0;
}

Both compilers have __has_c_attribute in both dialect modes, and both answer 0 for unsequenced in these versions. On GCC 15 the last line changes to 1, because GCC implements the attribute; Clang 23 still answers 0.

$ gcc -std=c2x attributes.c -o attributes
$ ./attributes
__STDC_VERSION__ = 202000L
__has_c_attribute = 1
[[unsequenced]] = 0
$ clang -std=c23 attributes.c -o attributes
$ ./attributes
__STDC_VERSION__ = 202311L
__has_c_attribute = 1
[[unsequenced]] = 0

What current releases support

The installed compilers are well behind the current releases, so the comparison above shows the past. The current releases are GCC 16.2 and Clang 23.1.5, and their status pages give the minimum version for each C23 feature. The table merges both pages for the features that differ.

FeatureGCCClang
C23 keywords and nullptr1317
typeof, typeof_unqual1316
constexpr object definitions1319
auto type inference1318
_BitInt(N)1415
#embed1519
[[unsequenced]], [[reproducible]]15not implemented
Storage class specifiers in compound literals13not implemented
Decimal floating-point types4.3not implemented
Improved tag compatibility14partial since 21
Default -stdgnu23 since 15gnu17
C23 overallsupportedpartial

The decimal floating types and the interchange types of Annex H are optional in C23. GCC implements them on IA-32 and x86-64 since GCC 4.3 and on AArch64 since GCC 13; Clang does not implement them at all, and its Clang 23 release notes still say so when they diagnose the H, D and DD length modifiers. _BitInt is a core feature, but GCC 14 implements it only on IA-32, x86-64 and little-endian AArch64, while Clang has supported it since Clang 15. Everything else in the table is from the two official status pages, as of November 14, 2026.

C2y, the next standard

C2y is the working name of the revision after C23. It is not published, and both compilers select it with -std=c2y: GCC 15 was the first GCC release with the option, Clang 19 the first Clang, and the installed Clang 18 still rejects the name. Support in both is experimental and incomplete, so a program that relies on it must pin the compiler version. Many of the features discussed in WG14 are small clean-ups of the language, and both projects implement a different subset.

GCC 15 added if declarations, which declare a variable in the condition, and named loops. GCC 16 added _Countof and static assertions in expressions. Clang 21 added _Countof, Clang 22 named loops, and if declarations are listed for Clang 24. Clang also implements several smaller papers, such as delimited escape sequences in Clang 21 and __COUNTER__ in Clang 22, while both still list some papers as unimplemented. For an unfinished standard the ranking is therefore unstable, and the useful test is not a compiler name but __STDC_VERSION__ plus a build of the actual feature.

Which compiler supports the new standards better?

For the published standard, GCC. Three facts decide it. First, C23 is complete in GCC: every language feature in the standard has an entry on the GCC status page and none is marked unimplemented, while Clang's page still labels C23 "partial" and lists missing features, including [[unsequenced]], storage class specifiers in compound literals, decimal floating types and Annex H. Second, GCC 15 changed the default dialect to gnu23, so C23 code builds without any flags, whereas Clang still defaults to gnu17 and rejects the same code unless -std=c23 is added. Third, GCC's remaining C23 limitation is target-specific rather than missing: _BitInt and decimal types exist only on some architectures in older releases.

Clang is not behind in every area. It implemented _BitInt a release before GCC, it has the better diagnostics, and its sanitizers, clang-tidy, clangd and coverage tools are stronger than the GCC equivalents. It also implements many C2y papers early, so a project that tracks the draft may find a feature in Clang first. For code that targets ISO C23, however, GCC is the compiler that accepts more of the standard, with fewer flags and fewer warnings.

Building with both compilers

The practical answer is to build with both and let the stronger standard support catch the mistakes. The script below compiles one source file with each compiler using the newest spelling the compiler understands, with warnings treated as errors. The probe is needed because GCC 13 does not know -std=c23; on current compilers the fallback never runs.

check.sh
#!/bin/sh
# build one source file with both compilers

for cc in gcc clang; do
    if ! "$cc" -std=c23 -Wall -Wextra -Werror -o probe "$1" 2>/dev/null; then
        "$cc" -std=c2x -Wall -Wextra -Werror -o probe "$1" || exit 1
    fi
    printf '%-6s' "$cc"
    ./probe
done

rm -f probe

Run against the first program, the script reports the dialect each compiler selected. Adding -pedantic-errors turns every GNU extension into an error as well, which is what a project that claims ISO conformance wants. The compiler output is hidden for the probe only; in a real build it should be visible, and a CI job should run the same matrix so that the older and the newer release are both tested.

$ ./check.sh standards.c
gcc   __STDC_VERSION__ = 202000L
clang __STDC_VERSION__ = 202311L

Source

C Standards Support in GCC
C Support in Clang
GCC 15 Release Notes: C23 by default
Clang 23.1.0 Release Notes
cppreference: C23

This tutorial compared GCC and Clang on the C standards, from the default dialect and the C23 keywords to _BitInt, #embed and the C2y draft, and found GCC ahead for the published C23. The examples are standard C, so any conforming compiler accepts them once it implements the feature under test; for the library functions used in them see the C strlen function and the C printf function tutorials.

Author

My name is Jan Bodnar, and I'm a dedicated programmer with a deep passion for coding. Since 2007, I've been sharing my expertise through over 1,400 articles and 8 e-books. With more than a decade of teaching experience, I strive to make programming accessible and engaging.

List C Standard Library.