Het artikel bespreekt een technische ambiguïteit in de C89 (C90) standaard over implicit function declarations. Wanneer een functie wordt aangeroepen zonder voorafgaande declaratie, wordt deze impliciet gedefinieerd als een functie die een int teruggeeft.
De auteur toont aan dat GCC en Clang verschillend interpreteren hoe dit werkt in complexe scenario's (zoals binnen array-declaratoren), wat terug te voeren is op de onduidelijke term "innermost block" in de officiële ANSI-standaard. Omdat deze functionaliteit in C99 is verwijderd, zal er nooit een definitieve oplossing komen voor dit meningsverschil tussen compilers. Het artikel eindigt met een pleidooi om over te stappen op moderne standaarden zoals C11.
Een ambiguïteit in C89 die nooit wordt opgelost
Ik heb enkele ambigue formuleringen gevonden in de C89(/C90) standaard, waarbij GCC en Clang van mening verschillen over de interpretatie. Het gaat om het gedrag van impliciete functieverklaringen (implicit function declarations), die in C99 zijn verwijderd, waardoor dit nooit definitief is opgehelderd.
Wat zijn impliciete functieverklaringen?
Voor wie niet bekend is met deze functie: C89 heeft een bijzonder kenmerk waarbij, als je een functie probeert aan te roepen die niet bestaat, de compiler geen foutmelding geeft maar de functie impliciet declareert als een functie met ongespecificeerde parameters die een int teruggeeft.
Specifiek: als de functie in een aanroep-expressie een identifier is zonder haakjes die niet binnen het huidige bereik (scope) valt, wordt deze in het bereik geplaatst als een extern int ().
Een opvallend detail hierbij is dat schijnbaar overbodige haakjes rondom expressies de semantiek beïnvloeden: f() plaatst f in het bereik, maar (f)() doet dat niet.
Een eenvoudig voorbeeld
Om tot de kern te komen, beginnen we met een simpel voorbeeld. Hier is een declaratie waarvan de declarator zichzelf declareert:
int f(int [sizeof(f())]);
Dit declareert een functie met één parameter, wat een array is (die vervalt tot een pointer). Identifiers worden in het bereik geplaatst nadat de declarator is voltooid. Wanneer f() dus wordt aangeroepen binnen de array-declarator, is deze nog niet in het bereik en wordt hij gedeclareerd als int (). De declarator wordt vervolgens afgerond en f wordt opnieuw gedeclareerd met het compatibele type int (int *).
Het grensgeval
Nu pas ik de declaratie aan door één teken toe te voegen:
int f(int f[sizeof(f())]);
Is dit legaal?
Clang compileert dit probleemloos, terwijl GCC een foutmelding geeft. Wanneer compilers het oneens zijn over de semantiek, raadplegen we de officiële standaard:
ANSI X3-159-1989, 3.3.2.2 Function calls:
"If the expression that precedes the parenthesized argument list in a function call consists solely of an identifier, and if no declaration is visible for this identifier, the identifier is implicitly declared exactly as if, in the innermost block containing the function call, the declaration extern int identifier(); appeared."
Analyse van het "innermost block"
De cruciale zin hier is "innermost block". De standaard legt nooit uit wat dit precies betekent.
Intuïtief zou je denken dat dit slaat op block scope (bijvoorbeeld een compound statement), waardoor de code legaal zou moeten zijn. Echter, een declaratie kan ook voorkomen in file scope, buiten elk blok. Impliciete declaraties in file scope zijn nooit expliciet verboden en worden door zowel GCC als Clang toegestaan.
Een andere interpretatie is dat het eigenlijk "innermost scope" betekent. In dat geval zou f() gedeclareerd worden in de function prototype scope, binnen de functiedeclarator. Als dit het geval is, zou de parameter f een ongeldige herdeclaratie zijn (het herdeclareren van int () als int), en zou de code illegaal moeten zijn.
Een aanwijzing dat deze laatste interpretatie mogelijk onjuist is, is dat de impliciete declaratie gespecificeerd is in de vorm extern int (). De extern-specifier is niet toegestaan binnen de function prototype scope. Misschien is die scope dus juist uitgesloten. Mogelijk betekent "innermost block" eigenlijk innermost block scope of file scope, waardoor de code wel degelijk legaal is.
Verdere divergentie in gedrag
Hier is een ander voorbeeld dat bevestigt dat het verschil in gedrag voortkomt uit de vraag in welk bereik de impliciete declaratie terechtkomt. In dit geval geeft Clang een foutmelding en slaagt GCC (het tegenovergestelde van het vorige voorbeeld):
int main(void)
{
int f(int [sizeof(x())]);
int x;
}
Daarnaast is opvallend dat, in GCC, de impliciete declaratie in function prototype scope niet vervalt tot een pointer, hoewel functietypes in function prototype scope normaal gesproken onmogelijk zijn. GCC geeft een foutmelding bij het compileren van het volgende, omdat f wordt herdeclareerd als int (*)():
int f(int [sizeof(f())], int f());
Dit is nog een aanwijzing dat het gedrag van Clang wellicht "correcter" is.
Conclusie
Omdat dit problematische gedrag ruim 27 jaar geleden uit de standaard is verwijderd, zullen we nooit uitsluitsel krijgen over wat de bedoelde interpretatie was.
***
Postscriptum: Ik smeek mensen om te stoppen met het gebruik van C89. Alsjeblieft. C11 is inmiddels meer dan 15 jaar geleden gefinaliseerd; er is genoeg tijd verstreken, ik beloof je dat het goed komt.
"Maar ik schrijf code voor een niche-microprocessor gemaakt door een bedrijf dat 25 jaar geleden failliet is gegaan en die alleen wordt ondersteund door deze ene propriëtaire C89-compiler, en de machine zal zichzelf opblazen als je iets anders probeert te gebruiken." Kijk, als je echt een reden hebt waarom je geen andere optie hebt, dan weet je wie je bent. De meeste mensen hebben geen enkel excuus.
Een ambiguïteit in C89 die nooit wordt opgelost
Ik heb enkele ambigue formuleringen gevonden in de C89(/C90) standaard, waarbij GCC en Clang van mening verschillen over de interpretatie. Het gaat om het gedrag van impliciete functieverklaringen (implicit function declarations), die in C99 zijn verwijderd, waardoor dit nooit definitief is opgehelderd.
Wat zijn impliciete functieverklaringen?
Voor wie niet bekend is met deze functie: C89 heeft een bijzonder kenmerk waarbij, als je een functie probeert aan te roepen die niet bestaat, de compiler geen foutmelding geeft maar de functie impliciet declareert als een functie met ongespecificeerde parameters die een int teruggeeft.
Specifiek: als de functie in een aanroep-expressie een identifier is zonder haakjes die niet binnen het huidige bereik (scope) valt, wordt deze in het bereik geplaatst als een extern int ().
Een opvallend detail hierbij is dat schijnbaar overbodige haakjes rondom expressies de semantiek beïnvloeden: f() plaatst f in het bereik, maar (f)() doet dat niet.
Een eenvoudig voorbeeld
Om tot de kern te komen, beginnen we met een simpel voorbeeld. Hier is een declaratie waarvan de declarator zichzelf declareert:
int f(int [sizeof(f())]);
Dit declareert een functie met één parameter, wat een array is (die vervalt tot een pointer). Identifiers worden in het bereik geplaatst nadat de declarator is voltooid. Wanneer f() dus wordt aangeroepen binnen de array-declarator, is deze nog niet in het bereik en wordt hij gedeclareerd als int (). De declarator wordt vervolgens afgerond en f wordt opnieuw gedeclareerd met het compatibele type int (int *).
Het grensgeval
Nu pas ik de declaratie aan door één teken toe te voegen:
int f(int f[sizeof(f())]);
Is dit legaal?
Clang compileert dit probleemloos, terwijl GCC een foutmelding geeft. Wanneer compilers het oneens zijn over de semantiek, raadplegen we de officiële standaard:
ANSI X3-159-1989, 3.3.2.2 Function calls:
"If the expression that precedes the parenthesized argument list in a function call consists solely of an identifier, and if no declaration is visible for this identifier, the identifier is implicitly declared exactly as if, in the innermost block containing the function call, the declaration extern int identifier(); appeared."
Analyse van het "innermost block"
De cruciale zin hier is "innermost block". De standaard legt nooit uit wat dit precies betekent.
Intuïtief zou je denken dat dit slaat op block scope (bijvoorbeeld een compound statement), waardoor de code legaal zou moeten zijn. Echter, een declaratie kan ook voorkomen in file scope, buiten elk blok. Impliciete declaraties in file scope zijn nooit expliciet verboden en worden door zowel GCC als Clang toegestaan.
Een andere interpretatie is dat het eigenlijk "innermost scope" betekent. In dat geval zou f() gedeclareerd worden in de function prototype scope, binnen de functiedeclarator. Als dit het geval is, zou de parameter f een ongeldige herdeclaratie zijn (het herdeclareren van int () als int), en zou de code illegaal moeten zijn.
Een aanwijzing dat deze laatste interpretatie mogelijk onjuist is, is dat de impliciete declaratie gespecificeerd is in de vorm extern int (). De extern-specifier is niet toegestaan binnen de function prototype scope. Misschien is die scope dus juist uitgesloten. Mogelijk betekent "innermost block" eigenlijk innermost block scope of file scope, waardoor de code wel degelijk legaal is.
Verdere divergentie in gedrag
Hier is een ander voorbeeld dat bevestigt dat het verschil in gedrag voortkomt uit de vraag in welk bereik de impliciete declaratie terechtkomt. In dit geval geeft Clang een foutmelding en slaagt GCC (het tegenovergestelde van het vorige voorbeeld):
int main(void)
{
int f(int [sizeof(x())]);
int x;
}
Daarnaast is opvallend dat, in GCC, de impliciete declaratie in function prototype scope niet vervalt tot een pointer, hoewel functietypes in function prototype scope normaal gesproken onmogelijk zijn. GCC geeft een foutmelding bij het compileren van het volgende, omdat f wordt herdeclareerd als int (*)():
int f(int [sizeof(f())], int f());
Dit is nog een aanwijzing dat het gedrag van Clang wellicht "correcter" is.
Conclusie
Omdat dit problematische gedrag ruim 27 jaar geleden uit de standaard is verwijderd, zullen we nooit uitsluitsel krijgen over wat de bedoelde interpretatie was.
***
Postscriptum: Ik smeek mensen om te stoppen met het gebruik van C89. Alsjeblieft. C11 is inmiddels meer dan 15 jaar geleden gefinaliseerd; er is genoeg tijd verstreken, ik beloof je dat het goed komt.
"Maar ik schrijf code voor een niche-microprocessor gemaakt door een bedrijf dat 25 jaar geleden failliet is gegaan en die alleen wordt ondersteund door deze ene propriëtaire C89-compiler, en de machine zal zichzelf opblazen als je iets anders probeert te gebruiken." Kijk, als je echt een reden hebt waarom je geen andere optie hebt, dan weet je wie je bent. De meeste mensen hebben geen enkel excuus.