91 votes

Les branches au comportement indéfini peuvent-elles être considérées comme inaccessibles et optimisées comme du code mort ?

Considérez l'affirmation suivante :

*((char*)NULL) = 0; //undefined behavior

Il invoque clairement un comportement non défini. L'existence d'une telle instruction dans un programme donné signifie-t-elle que l'ensemble du programme est indéfini ou que le comportement ne devient indéfini que lorsque le flux de contrôle atteint cette instruction ?

Le programme suivant serait-il bien défini dans le cas où l'utilisateur n'entre jamais le nombre 3 ?

while (true) {
 int num = ReadNumberFromConsole();
 if (num == 3)
  *((char*)NULL) = 0; //undefined behavior
}

Ou s'agit-il d'un comportement totalement indéfini, quel que soit ce que l'utilisateur saisit ?

De même, le compilateur peut-il supposer qu'un comportement non défini ne sera jamais exécuté au moment de l'exécution ? Cela permettrait de raisonner à rebours dans le temps :

int num = ReadNumberFromConsole();

if (num == 3) {
 PrintToConsole(num);
 *((char*)NULL) = 0; //undefined behavior
}

Ici, le compilateur pourrait raisonner qu'au cas où num == 3 nous invoquerons toujours un comportement non défini. Par conséquent, ce cas doit être impossible et le nombre n'a pas besoin d'être imprimé. L'ensemble du if pourrait être optimisé. Ce type de raisonnement à rebours est-il autorisé par la norme ?

69voto

Steve Jessop Points 166970

Est-ce que l'existence d'une telle déclaration dans un programme donné signifie que que tout le programme est indéfini ou que le comportement devient indéfini seulement que lorsque le flux de contrôle atteint cette instruction ?

Ni l'un ni l'autre. La première condition est trop forte et la seconde est trop faible.

Les accès aux objets sont parfois séquencés, mais la norme décrit le comportement du programme en dehors du temps. Danvil a déjà cité :

si une telle exécution contient une opération non définie, cette norme internationale n'impose aucune exigence à l'implémentation l'exécution de ce programme avec cette entrée (même pas en ce qui concerne les opérations précédant la première opération non définie)

Cela peut être interprété :

Si l'exécution du programme donne lieu à un comportement non défini, alors le programme entier a comportement indéfini.

Ainsi, une déclaration inaccessible avec UB ne donne pas au programme UB. Un énoncé atteignable qui (à cause des valeurs des entrées) n'est jamais atteint, ne donne pas le programme UB. C'est pourquoi votre première condition est trop forte.

Maintenant, le compilateur ne peut pas, en général, dire ce qu'est l'UB. Ainsi, pour permettre à l'optimiseur de réordonner des instructions avec un potentiel UB qui serait réordonnable si leur comportement était défini, il est nécessaire de permettre à l'UB de "remonter dans le temps" et de se tromper avant le point de séquence précédent (ou dans la terminologie C++11, pour que l'UB affecte les choses qui sont séquencées avant la chose UB). Par conséquent, votre deuxième condition est trop faible.

Un exemple majeur de ceci est lorsque l'optimiseur s'appuie sur un aliasing strict. L'intérêt des règles d'aliasing strict est de permettre au compilateur de réordonner des opérations qui ne pourraient pas être réordonnées valablement s'il était possible que les pointeurs en question aliasent la même mémoire. Ainsi, si vous utilisez des pointeurs à alias illégal, et que UB se produit, alors il peut facilement affecter une déclaration "avant" la déclaration UB. En ce qui concerne la machine abstraite, l'instruction UB n'a pas encore été exécutée. En ce qui concerne le code objet réel, il a été partiellement ou totalement exécuté. Mais la norme n'essaie pas d'entrer dans les détails de ce que cela signifie pour l'optimiseur de réordonner les instructions, ou quelles en sont les implications pour UB. Elle donne simplement à l'implémentation la permission de se tromper dès qu'elle le souhaite.

Vous pouvez penser à ça comme à "UB a une machine à remonter le temps".

Pour répondre spécifiquement à vos exemples :

  • Le comportement est seulement indéfini si 3 est lu.
  • Les compilateurs peuvent éliminer du code comme étant mort si un bloc de base contient une opération dont il est certain qu'elle est indéfinie. Ils sont autorisés (et je suppose qu'ils le font) dans les cas qui ne sont pas un bloc de base mais où toutes les branches mènent à UB. Cet exemple n'est pas un candidat à moins que PrintToConsole(3) est en quelque sorte connu pour être sûr de revenir. Il pourrait lancer une exception ou autre.

Un exemple similaire à votre deuxième est l'option gcc -fdelete-null-pointer-checks qui peut prendre le code suivant (je n'ai pas vérifié cet exemple spécifique, je le considère comme illustratif de l'idée générale) :

void foo(int *p) {
    if (p) *p = 3;
    std::cout << *p << '\n';
}

et le changer en :

*p = 3;
std::cout << "3\n";

Pourquoi ? Parce que si p est nul alors le code a UB de toute façon, donc le compilateur peut supposer qu'il n'est pas nul et l'optimiser en conséquence. Le noyau linux a trébuché sur ce point ( https://web.nvd.nist.gov/view/vuln/detail?vulnId=CVE-2009-1897 ) essentiellement parce qu'il fonctionne dans un mode où le déréférencement d'un pointeur nul n'est pas est censé être UB, il est censé résulter en une exception matérielle définie que le noyau peut gérer. Lorsque l'optimisation est activée, gcc demande l'utilisation de -fno-delete-null-pointer-checks afin de fournir cette garantie hors normes.

P.S. La réponse pratique à la question "quand le comportement indéfini frappe-t-il ?" est "10 minutes avant que vous ne prévoyiez de partir pour la journée".

10voto

Danvil Points 9443

La norme stipule à 1.9/4

[Note : La présente Norme internationale n'impose aucune exigence quant au comportement des programmes qui contiennent un comportement non défini. - note de fin ]

Le point intéressant est probablement ce que signifie "contenir". Un peu plus loin, à 1.9/5, il est écrit :

Toutefois, si une telle exécution contient une opération non définie, cette norme internationale n'impose aucune exigence à l'implémentation l'exécution de ce programme avec cette entrée (pas même en ce qui concerne les opérations précédant la première opération non définie)

Ici, il est spécifiquement mentionné "exécution ... avec cette entrée". J'interpréterais cela comme un comportement non défini dans une branche possible qui n'est pas exécutée pour le moment et qui n'influence pas la branche d'exécution actuelle.

Les hypothèses basées sur un comportement non défini lors de la génération du code constituent toutefois un problème différent. Voir la réponse de Steve Jessop pour plus de détails à ce sujet.

5voto

Zack Points 44583

Un exemple instructif est

int foo(int x)
{
    int a;
    if (x)
        return a;
    return 0;
}

Les GCC et Clang actuels optimiseront ceci (sur x86) pour

xorl %eax,%eax
ret

parce qu'ils déduisez que x est toujours nul de l'UB dans le if (x) chemin de contrôle. GCC ne vous donnera même pas un avertissement d'utilisation de valeur non initialisée ! (parce que la passe qui applique la logique ci-dessus s'exécute avant la passe qui génère les avertissements de valeurs non initialisées).

4voto

Jens Points 1046

L'actuel projet de travail C++ stipule en 1.9.4 que

La présente Norme internationale n'impose aucune exigence quant au comportement des programmes qui contiennent des comportements non définis.

Sur cette base, je dirais qu'un programme contenant un comportement indéfini sur n'importe quel chemin d'exécution peut faire n'importe quoi à chaque instant de son exécution.

Il existe deux très bons articles sur les comportements indéfinis et sur ce que font généralement les compilateurs :

3voto

n.m. Points 30344

Le mot "comportement" signifie que quelque chose est fait . Une déclaration qui n'est jamais exécutée n'est pas un "comportement".

Une illustration :

*ptr = 0;

Est-ce un comportement indéfini ? Supposons que nous soyons 100% certains ptr == nullptr au moins une fois pendant l'exécution du programme. La réponse devrait être oui.

Et ça ?

 if (ptr) *ptr = 0;

Est-ce indéfini ? (Rappelez-vous ptr == nullptr au moins une fois ?) J'espère bien que non, sinon vous ne serez pas en mesure d'écrire un programme utile du tout.

Aucun srandardais n'a été blessé dans la réalisation de cette réponse.

Prograide.com

Prograide est une communauté de développeurs qui cherche à élargir la connaissance de la programmation au-delà de l'anglais.
Pour cela nous avons les plus grands doutes résolus en français et vous pouvez aussi poser vos propres questions ou résoudre celles des autres.

Powered by:

X