C «comportement observable» dans le contexte de UB «comportement indéfini»

(La question a été posée à l’origine par les commentaires de cette réponse à Y a-t-il des conditions de concurrence dans cette implémentation producteur-consommateur?

Considérez ce code minimal:

#define BUFSIZ 10 char buf[BUFSIZ]; void f(int *pn) { buf[*pn]++; *pn = (*pn + 1) % BUFSIZ; } int main() { int n = 0; f(&n); return n; } 

Question: les règles C “en tant que si” permettent-elles au compilateur de réécrire le code comme suit?

 void f(int *pn) { int n = *pn; *pn = (*pn + 1) % BUFSIZ; buf[n]++; } 

D’une part, ce qui précède ne changerait pas le comportement observable du programme tel qu’il est écrit.

Par contre, f pourrait être appelé avec un index invalide, éventuellement depuis une autre unité de traduction:

 int g() { int n = -1001; f(&n); } 

Dans ce dernier cas, les deux variantes du code invoqueraient UB lors de l’access à l’élément de tableau hors limites. Cependant, le code d’origine laisserait *pn à la valeur transmise à f (= -1001) tandis que le code réécrit entrerait dans UB-land seulement après avoir modifié *pn (en 0 ).

Est-ce qu’une telle différence compterait comme “observable” ou, pour revenir à la question réelle, y a-t-il quelque chose dans la norme C qui autoriserait ou empêcherait spécifiquement ce type de réécriture / optimisation de code?

  1. Si une partie de votre programme a un comportement indéfini, le comportement de l’ensemble du programme est indéfini. En d’autres termes, le comportement du programme est indéfini même “avant” toute construction dont le comportement est indéfini. (Cela est nécessaire pour permettre au compilateur d’effectuer certaines optimisations dépendant du comportement en cours de définition.)

  2. Étant donné qu’aucune des variables n’est déclarée volatile, il est possible que l’ordre des mises à jour de la mémoire soit réorganisé comme indiqué, car le comportement observable n’est garanti conformément au modèle d’exécution qu’en l’absence de comportement indéfini.

  3. Le “comportement observable” (dans la norme C) est défini au § 5.1.2.3 comme suit:

    • Les access aux objects volatils sont évalués ssortingctement selon les règles de la machine abstraite.
    • A la fin du programme, toutes les données écrites dans les fichiers doivent être identiques au résultat obtenu par l’exécution du programme conformément à la sémantique abstraite.
    • La dynamic d’entrée et de sortie des dispositifs interactifs doit se dérouler comme spécifié en 7.21.3. Le but de ces exigences est que la sortie non tamponnée ou avec tampon de ligne apparaisse dès que possible, afin de garantir que les messages d’invite apparaissent avant qu’un programme attende une entrée.

    Cette liste n’inclut aucune réponse potentielle à un comportement indéfini (tel qu’un piège ou un signal), même si en général, une erreur de segmentation est normalement observable. L’exemple particulier de la question n’implique aucun de ces trois points. (L’UB pourrait empêcher le programme de s’achever avec succès, ce qui annule fondamentalement le deuxième point du comportement observable.) Ainsi, dans le cas particulier du code de la question, la réorganisation ne modifierait aucun comportement observable et pourrait clairement être effectuée.

  4. Mon affirmation selon laquelle la réponse de l’implémentation à un comportement indéfini ne se limite pas à une exécution ssortingctement conforme au composant qui engendre un comportement indéfini a créé beaucoup plus de controverse que prévu dans le fil de commentaires, car il s’agit d’une fonctionnalité assez connue du C moderne. Il vaut la peine de revoir l’ essai utile de John Regehr sur le comportement indéfini , dont je cite: (dans la troisième partie)

    Plus précisément, quand un programme meurt après avoir effectué une opération illégale, telle que la division par zéro ou le déréférencement d’un pointeur nul, cela est-il considéré comme un effet secondaire? La réponse est clairement non. Puisque les opérations induisant des collisions n’ont pas d’effet secondaire, le compilateur peut les réorganiser par rapport à d’autres opérations.

    Comme exemple peut-être plus intéressant (tiré du fil de commentaires), si un programme produit plusieurs lignes de sortie, puis exécute délibérément une division explicite par zéro, on peut s’attendre à ce que la compilation et l’exécution du programme produisent la sortie avant de répondre. de quelque manière que ce soit, il répond à la division par zéro. Cependant, un compilateur qui a détecté la division par zéro et pourrait prouver que le stream de contrôle du programme garantissait son exécution aurait parfaitement le droit de produire un message d’erreur au moment de la traduction et de refuser de produire une image exécutable.

    Sinon, s’il ne pouvait pas prouver que le stream de contrôle atteignait la division par zéro, il serait en droit de supposer que la division par zéro ne pourrait pas se produire et, par conséquent, de supprimer de manière non équivoque tout le code menant à la division par zéro. (y compris les appels à la fonction de sortie) en tant que code mort.

    Les deux réponses ci-dessus entrent dans la liste des exemples de réponses au comportement non défini au § 3.4.3: “d’ignorer complètement la situation avec des résultats imprévisibles,… jusqu’à mettre fin à une traduction ou à une exécution (avec l’émission d’un message de diagnostic)”.