Contexte historique du langage C
Le standard C89 a été rédigé pour couvrir une gamme très large de matériels, dont des machines à représentation sign‑magnitude, des pointeurs segmentés et même des octets de neuf bits. Pour garantir la portabilité, le texte a introduit une machine abstraite dont le comportement observable devait être reproduit par toute implémentation. Cette abstraction laisse aux compilateurs une marge de manœuvre importante sur tout ce qui n’est pas « observable », notamment les accès aux variables non volatiles.
Origine et portée du comportement indéfini
Le standard classe certaines constructions comme undefined behavior (UB) lorsqu’il ne spécifie aucune sémantique. Cette liberté permet aux implémentations d’ajouter des extensions matérielles, d’appliquer des optimisations agressives et d’ignorer des erreurs difficiles à détecter. En pratique, un programme contenant du UB peut être traité comme n’ayant aucune sémantique attendue, ce qui conduit parfois à des transformations dangereuses, comme la suppression de tests de division par zéro.
extern int x;
int f(int a, int b) {
x = b ? 42 : 43;
return a/b; // division par zéro si b==0 → UB
}
Dans cet exemple, certains compilateurs éliminent le test et exécutent directement x = 42, car le chemin b==0 est considéré comme non observable. Le même code, mais avec un appel de fonction observable, expose une divergence :
extern void g(int x);
int f(int a, int b) {
g(b ? 42 : 43);
return a/b;
}
Si g peut déclencher exit() lorsque b==0, la suppression du test change le comportement observable, ce qui constitue un bug de compilation.
Évolutions récentes du standard
Le projet C2y, version en cours de finalisation, recense environ 100 cas d’UB dans le standard actuel et en a déjà éliminé 45. Parmi les nouveautés, la clause « no time travel » empêche le déplacement d’opérations au‑delà d’un accès volatile, corrigeant ainsi des optimisations qui « voyageaient dans le temps » :
volatile int x;
int foo(int a, int b, bool store_to_x) {
if (!store_to_x) return a/b;
x = b;
return a/b; // le compilateur ne doit plus hoister ce division
}
En C++, la même contrainte s’obtient via std::observable_checkpoint(), mais le C23 introduit directement la règle dans le langage, réduisant le besoin de constructions explicites.
Outils de détection et perspectives
Le nombre d’outils capables de repérer du UB augmente : les avertissements du compilateur (GCC, Clang) sont plus précis, les analyseurs statiques (Cppcheck, Clang‑Static‑Analyzer) signalent des lectures de variables non initialisées, et les sanitizers (UBSan, AddressSanitizer) instrumentent le code à l’exécution. De plus, des solutions basées sur les grands modèles de langage (LLM) proposent des suggestions de correction, tandis que la vérification formelle explore la preuve de l’absence d’UB dans des modules critiques. Malgré ces avancées, la spécification reste complexe ; la plupart des études de cas cités proviennent de présentations de conférences et de sondages internes, sans métriques publiques détaillées.
Les trois groupes d’étude du comité C – modèle d’objet mémoire, sécurité mémoire et UB – poursuivent la réduction du nombre de cas indéfinis. Leur travail, combiné à l’adoption croissante d’outils de diagnostic, indique une tendance à la stabilisation du langage, même si la compatibilité ascendante impose des compromis.