Je C is geen standaard C

"Het comité heeft een onrealistische taal gecreëerd die niemand kan of zal gebruiken."
— Dennis Ritchie (uit context)

Dit is een onvolledige lijst van enkele incompatibiliteiten tussen C zoals beschreven in de standaard en C zoals het bestaat in de echte wereld voor mensen die gebruikmaken van gcc/clang + glibc/musl.

asm

Standaard herkennen gcc en clang het trefwoord asm als een extensie. Dit is strikt genomen niet conform, omdat asm in standaard C een geldige, niet-gereserveerde identifier is. Code die dus voldoet aan de standaard, is mogelijk niet te compileren met gcc/clang bij hun standaardinstellingen.

Hoewel dit opgelost kan worden met -std, is het opvallend dat compilers standaard niet conform zijn. Daarnaast rijst de vraag waarom de standaard asm niet simpelweg als een gereserveerd woord heeft gedefinieerd; C++ doet dit wel, maar C niet.

FLT_ROUNDS

De standaard specificeert de FLT_ROUNDS macro in float.h als volgt:

5.3.5.3.3p23: De afrondingsmodus voor floating-point optelling voor standaard floating-types wordt gekenmerkt door de implementatie-afhankelijke waarde van FLTROUNDS. De evaluatie van FLTROUNDS weerspiegelt correct elke wijziging van de afrondingsmodus tijdens de uitvoering via de functie fesetround in <fenv.h>.

Opvallend is dat de standaard vereist dat de waarde dynamisch verandert met de runtime floating environment. GCC definieert FLT_ROUNDS echter altijd als 1 (afronden naar de dichtstbijzijnde), ongeacht de afrondingsmodus tijdens runtime:

/* Addition rounds to 0: zero, 1: nearest, 2: +inf, 3: -inf, -1: unknown.  */
/* ??? This is supposed to change with calls to fesetround in <fenv.h>.  */
#undef FLT_ROUNDS
#define FLT_ROUNDS 1

Clang definieert hier een builtin voor (__builtinfltrounds()), maar gcc doet dit om onbekende redenen niet.

Standaard pragmas

De C-standaard specificeert vijf standaard pragmas om het gedrag van floating-point rekenkunde te beheren:

Toegevoegd in C99 (switch kan ON, OFF, of DEFAULT zijn):

#pragma STDC FP_CONTACT ON
#pragma STDC FENV_ACCESS ON
#pragma STDC CX_LIMITED_RANGE ON

Toegevoegd in C23 (direction kan FEDYNAMIC, FEDOWNWARD, FETONEAREST, FETONEARESTFROMZERO, FETOWARDZERO, FEUPWARD, of een andere implementatie-afhankelijke waarde zijn):

#pragma STDC FENV_ROUND FE_DYNAMIC
#pragma STDC FENV_DEC_ROUND FE_DYNAMIC

Echter, meer dan 27 jaar nadat C99 werd gestandaardiseerd, ondersteunt gcc nog steeds geen enkele van de standaard pragmas. In gcc zijn ze simpelweg "no-ops".

Een deel (maar niet alles) van de functionaliteit kan worden gereproduceerd met command-line flags, maar deze werken op de gehele translatie-eenheid, terwijl pragmas kunnen worden toegepast op individuele functies en blokken.

Clang ondersteunt de C99-pragmas grotendeels, maar ondersteunt de C23-pragmas nog niet.

Kwalificaties in return-types van functies

De standaard vereist dat kwalificaties in return-types van functies worden genegeerd:

Indien in de declaratie "T D1", D1 de vorm heeft D ( parameter-type-listopt ) attribute-specifier-sequenceopt en het type gespecificeerd voor ident in de declaratie "T D" is "derived-declarator-type-list T", dan is het type gespecificeerd voor ident "derived-declarator-type-list functie die de ongekwalificeerde, niet-atomische versie van T retourneert". De optionele attribute specifier hoort bij het functietype.

Clang behandelt deze echter als significant:

$ cat t.c
int f();
const int f();
$ clang t.c
t.c:2:11: error: conflicting types for 'f'
 2 | const int f();
   |           ^
t.c:1:5: note: previous declaration is here
 1 | int f();
   |     ^
 1 error generated.

De twee declaraties zouden identiek moeten zijn, maar clang beschouwt ze als incompatibel.

Verouderde va_start definitie in musl

Om redenen die onduidelijk zijn, levert musl eigen versies van sommige headers die de compiler zelf al levert. Een daarvan is stdarg.h, die ondersteund moet worden door freestanding implementaties en daarom vrijwel zeker al door de compiler wordt geleverd. Omdat het echter door musl wordt geleverd, zullen compilers die header gebruiken in plaats van de builtin versie.

Dit is problematisch omdat de header van musl strikt slechter is dan de builtin header. Dit is hoe musl va_start definieert:

#define va_start(v,l)   __builtin_va_start(v,l)

Deze definitie is onjuist, omdat C23 toestaat dat het tweede argument wordt weggelaten. Het zou als volgt gedefinieerd moeten worden:

#if __STDC_VERSION__ >= 202311L
#define va_start(...) __builtin_c23_va_start(__VA_ARGS__)
#else
#define va_start(v,l) __builtin_va_start(v,l)
#endif

Het resultaat is dat bij het compileren voor musl de single-argument va_start van C23 niet gebruikt kan worden, zelfs als dit wel wordt ondersteund door de compiler.

Zie ook

  • Aantekeningen over hoe glibc en musl rekenfouten afhandelen (math_errhandling)
  • Blijkbaar is het in standaard C niet mogelijk om portabel te controleren op fouten bij de conversie van string naar float: een geval waarin POSIX wellicht een familie van functies specificeert die incompatibel zijn met standaard C
  • Noch gcc noch clang zijn compliant met standaard C++: gcc en clang slaan taal-linkage informatie niet op bij functietypes in C++, in tegenstelling tot wat de standaard vereist
  • Nog een plek waar een Linux man-pagina onnauwkeurig is (bevat een verwijderde sectie uit dit bericht!)