Comportement indéfini, non spécifié et défini par l'implémentation

530

Quel est le comportement indéfini en C et C ++? Qu'en est-il du comportement non spécifié et du comportement défini par l'implémentation? Quelle est la différence entre eux?

Zolomon
la source
1
J'étais à peu près sûr que nous l'avions fait avant, mais je ne le trouve pas. Voir aussi: stackoverflow.com/questions/2301372/…
dmckee --- chaton ex-modérateur
1
Voici une discussion intéressante (la section "Annexe L et comportement indéfini").
Owen

Réponses:

407

Le comportement indéfini est l'un de ces aspects du langage C et C ++ qui peut surprendre les programmeurs venant d'autres langages (d'autres langages essaient de mieux le cacher). Fondamentalement, il est possible d'écrire des programmes C ++ qui ne se comportent pas de manière prévisible, même si de nombreux compilateurs C ++ ne signalent aucune erreur dans le programme!

Regardons un exemple classique:

#include <iostream>

int main()
{
    char* p = "hello!\n";   // yes I know, deprecated conversion
    p[0] = 'y';
    p[5] = 'w';
    std::cout << p;
}

La variable ppointe vers le littéral de chaîne "hello!\n"et les deux affectations ci-dessous tentent de modifier ce littéral de chaîne. Que fait ce programme? Selon le paragraphe 11 de la section 2.14.5 de la norme C ++, il invoque un comportement non défini :

L'effet de la tentative de modification d'un littéral de chaîne n'est pas défini.

Je peux entendre des gens crier "Mais attendez, je peux compiler cela sans problème et obtenir la sortie yellow" ou "Que voulez-vous dire non défini, les littéraux de chaîne sont stockés dans la mémoire en lecture seule, donc la première tentative d'affectation entraîne un vidage de mémoire". C'est exactement le problème avec un comportement non défini. Fondamentalement, la norme permet à tout ce qui se passe une fois que vous invoquez un comportement indéfini (même les démons nasaux). S'il y a un comportement "correct" selon votre modèle mental de la langue, ce modèle est tout simplement faux; La norme C ++ a le seul vote, point final.

D'autres exemples de comportement non défini incluent l'accès à un tableau au-delà de ses limites, le déréférencement du pointeur nul , l' accès aux objets après la fin de leur durée de vie ou l'écriture d' expressions prétendument intelligentes comme i++ + ++i.

La section 1.9 de la norme C ++ mentionne également les deux frères moins dangereux d'un comportement indéfini, un comportement non spécifié et un comportement défini par l'implémentation :

Les descriptions sémantiques de la présente Norme internationale définissent une machine abstraite non déterministe paramétrée.

Certains aspects et opérations de la machine abstraite sont décrits dans la présente Norme internationale comme définis par l' implémentation (par exemple sizeof(int)). Ceux-ci constituent les paramètres de la machine abstraite. Chaque mise en œuvre doit comprendre une documentation décrivant ses caractéristiques et son comportement à ces égards.

Certains autres aspects et opérations de la machine abstraite sont décrits dans la présente Norme internationale comme non spécifiés (par exemple, ordre d'évaluation des arguments d'une fonction). Lorsque cela est possible, la présente Norme internationale définit un ensemble de comportements autorisés. Celles-ci définissent les aspects non déterministes de la machine abstraite.

Certaines autres opérations sont décrites dans la présente Norme internationale comme indéfinies (par exemple, l'effet de déréférencer le pointeur nul). [ Remarque : la présente Norme internationale n'impose aucune exigence sur le comportement des programmes qui contiennent un comportement non défini. - note de fin ]

Plus précisément, la section 1.3.24 stipule:

Le comportement non défini autorisé va de l' ignorance complète de la situation avec des résultats imprévisibles , au comportement pendant la traduction ou l'exécution du programme d'une manière documentée caractéristique de l'environnement (avec ou sans émission d'un message de diagnostic), à la fin d'une traduction ou d'une exécution (avec l'émission d'un message de diagnostic).

Que pouvez-vous faire pour éviter de rencontrer un comportement non défini? Fondamentalement, vous devez lire de bons livres C ++ par des auteurs qui savent de quoi ils parlent. Vissez des didacticiels Internet. Vis bullschildt.

fredoverflow
la source
6
C'est un fait étrange qui résulte de la fusion que cette réponse ne couvre que C ++ mais les balises de cette question incluent C. C a une notion différente de "comportement indéfini": il faudra toujours l'implémentation pour donner des messages de diagnostic même si le comportement est également indiqué à ne pas être défini pour certaines violations de règles (violations de contraintes).
Johannes Schaub - litb
8
@Benoit C'est un comportement indéfini parce que la norme dit que c'est un comportement indéfini, point final. Sur certains systèmes, en effet, les littéraux de chaîne sont stockés dans le segment de texte en lecture seule, et le programme se bloque si vous essayez de modifier un littéral de chaîne. Sur d'autres systèmes, le littéral de chaîne apparaîtra en effet changer. La norme ne prescrit pas ce qui doit arriver. C'est ce que signifie un comportement indéfini.
fredoverflow
5
@FredOverflow, Pourquoi un bon compilateur nous permet-il de compiler du code qui donne un comportement non défini? Exactement ce que bon peut compiler ce genre de donner de code? Pourquoi tous les bons compilateurs ne nous ont-ils pas donné un énorme panneau d'avertissement rouge lorsque nous essayons de compiler du code qui donne un comportement non défini?
Pacerier
14
@Pacerier Certaines choses ne sont pas vérifiables au moment de la compilation. Par exemple, il n'est pas toujours possible de garantir qu'un pointeur nul n'est jamais déréférencé, mais cela n'est pas défini.
Tim Seguine
4
@Celeritas, un comportement non défini peut être non déterministe. Par exemple, il est impossible de savoir à l'avance quel sera le contenu de la mémoire non initialisée, par exemple. int f(){int a; return a;}: la valeur de apeut changer entre les appels de fonction.
Mark
97

Eh bien, il s'agit essentiellement d'un simple copier-coller de la norme

3.4.1 1 comportement défini par l'implémentation comportement non spécifié où chaque implémentation documente la façon dont le choix est fait

2 EXEMPLE Un exemple de comportement défini par l'implémentation est la propagation du bit de poids fort lorsqu'un entier signé est décalé vers la droite.

3.4.3 1 comportement indéfini comportement , lors de l'utilisation d'une construction de programme non portable ou erronée ou de données erronées, pour laquelle la présente Norme internationale n'impose aucune exigence

2 REMARQUE Le comportement indéfini possible va de l'ignorance complète de la situation avec des résultats imprévisibles, au comportement pendant la traduction ou l'exécution du programme d'une manière documentée caractéristique de l'environnement (avec ou sans émission d'un message de diagnostic), à la fin d'une traduction ou d'une exécution (avec l'émission d'un message de diagnostic).

3 EXEMPLE Un exemple de comportement non défini est le comportement en cas de dépassement d'entier.

3.4.4 1 comportement non spécifié utilisation d'une valeur non spécifiée, ou autre comportement lorsque la présente Norme internationale offre deux possibilités ou plus et n'impose aucune autre exigence sur laquelle est choisie dans tous les cas

2 EXEMPLE Un exemple de comportement non spécifié est l'ordre dans lequel les arguments d'une fonction sont évalués.

Fourmi
la source
3
Quelle est la différence entre un comportement défini par l'implémentation et un comportement non spécifié?
Zolomon
26
@Zolomon: Comme il est dit: fondamentalement la même chose, sauf que dans le cas d'une implémentation définie, l'implémentation est requise pour documenter (pour garantir) ce qui va se passer exactement, tandis qu'en cas d'indication non spécifiée, l'implémentation n'est pas tenue de documenter ou garantir quoi que ce soit.
AnT
1
@Zolomon: Cela se reflète dans la différence entre 3.4.1 et 2.4.4.
sbi
8
@Celeritas: les compilateurs hyper-modernes peuvent faire mieux que cela. Étant donné int foo(int x) { if (x >= 0) launch_missiles(); return x << 1; }qu'un compilateur peut déterminer que puisque tous les moyens d'invoquer la fonction qui ne lancent pas les missiles invoquent un comportement indéfini, il peut rendre l'appel launch_missiles()inconditionnel.
supercat
2
@northerner Comme l'indique la citation, un comportement non spécifié est généralement limité à un ensemble limité de comportements possibles. Dans certains cas, vous pourriez même conclure que toutes ces possibilités sont acceptables dans le contexte donné, auquel cas un comportement non spécifié n'est pas du tout un problème. Le comportement indéfini est totalement illimité (eb "le programme peut décider de formater votre disque dur"). Un comportement indéfini est toujours un problème.
AnT
60

Une formulation simple pourrait peut-être être plus facile à comprendre que la définition rigoureuse des normes.

comportement défini par l'implémentation
Le langage dit que nous avons des types de données. Les fournisseurs du compilateur spécifient les tailles à utiliser et fournissent une documentation de ce qu'ils ont fait.

comportement indéfini
Vous faites quelque chose de mal. Par exemple, vous avez une très grande valeur dans un intqui ne rentre pas char. Comment mettez-vous cette valeur char? en fait il n'y a aucun moyen! Tout pouvait arriver, mais le plus sensé serait de prendre le premier octet de cet entier et de le mettre char. Il est juste faux de faire cela pour affecter le premier octet, mais c'est ce qui se passe sous le capot.

comportement non spécifié
Quelle fonction de ces deux est exécutée en premier?

void fun(int n, int m);

int fun1()
{
  cout << "fun1";
  return 1;
}
int fun2()
{
  cout << "fun2";
  return 2;
}
...
fun(fun1(), fun2()); // which one is executed first?

La langue ne précise pas l'évaluation, de gauche à droite ou de droite à gauche! Par conséquent, un comportement non spécifié peut ou non entraîner un comportement non défini, mais votre programme ne doit certainement pas produire un comportement non spécifié.


@eSKay Je pense que votre question vaut la peine d'éditer la réponse pour clarifier davantage :)

car fun(fun1(), fun2());le comportement "implémentation n'est-il pas défini"? Le compilateur doit choisir l'un ou l'autre cours, après tout?

La différence entre l'implémentation définie et non spécifiée, est que le compilateur est censé choisir un comportement dans le premier cas, mais il n'a pas à le faire dans le second cas. Par exemple, une implémentation doit avoir une et une seule définition de sizeof(int). Donc, il ne peut pas dire que sizeof(int)c'est 4 pour une partie du programme et 8 pour d'autres. Contrairement au comportement non spécifié, où le compilateur peut dire OK, je vais évaluer ces arguments de gauche à droite et les arguments de la fonction suivante sont évalués de droite à gauche. Cela peut arriver dans le même programme, c'est pourquoi on l'appelle non spécifié . En fait, C ++ aurait pu être rendu plus facile si certains des comportements non spécifiés avaient été spécifiés. Jetez un œil ici à la réponse du Dr Stroustrup à cela :

On prétend que la différence entre ce qui peut être produit donnant au compilateur cette liberté et nécessitant une "évaluation ordinaire de gauche à droite" peut être significative. Je ne suis pas convaincu, mais avec d'innombrables compilateurs "là-bas" profitant de la liberté et certaines personnes défendant avec passion cette liberté, un changement serait difficile et pourrait prendre des décennies pour pénétrer dans les coins éloignés des mondes C et C ++. Je suis déçu que tous les compilateurs ne mettent pas en garde contre le code tel que ++ i + i ++. De même, l'ordre d'évaluation des arguments n'est pas spécifié.

OMI, beaucoup trop de «choses» restent indéfinies, non spécifiées, définies par l'implémentation, etc. Cependant, c'est facile à dire et même à donner des exemples, mais difficile à corriger. Il convient également de noter qu'il n'est pas si difficile d'éviter la plupart des problèmes et de produire du code portable.

AraK
la source
1
car fun(fun1(), fun2());le comportement n'est-il pas "implementation defined"? Le compilateur doit choisir l'un ou l'autre cours, après tout?
Lazer
1
@AraK: merci pour l'explication. Je le comprends maintenant. Btw, "I am gonna evaluate these arguments left-to-right and the next function's arguments are evaluated right-to-left"je comprends que cela canse produise. Est-ce vraiment le cas avec les compilateurs que nous utilisons de nos jours?
Lazer
1
@eSKay Vous devez demander à un gourou à ce sujet qui s'est sali les mains avec de nombreux compilateurs :) AFAIK VC évalue toujours les arguments de droite à gauche.
AraK
4
@Lazer: Cela peut certainement arriver. Scénario simple: foo (bar, boz ()) et foo (boz (), bar), où bar est un int et boz () est une fonction renvoyant int. Supposons un CPU où les paramètres devraient être passés dans les registres R0-R1. Les résultats des fonctions sont retournés dans R0; les fonctions peuvent supprimer R1. L'évaluation de "bar" avant "boz ()" nécessiterait d'enregistrer une copie de bar ailleurs avant d'appeler boz () puis de charger cette copie enregistrée. Évaluer "bar" après "boz ()" évitera un stockage de mémoire et une nouvelle extraction, et c'est une optimisation que de nombreux compilateurs feraient indépendamment de leur ordre dans la liste des arguments.
supercat
6
Je ne connais pas C ++ mais la norme C dit qu'une conversion d'un int en un char est soit définie par l'implémentation, soit même bien définie (en fonction des valeurs réelles et de la signature des types). Voir C99 §6.3.1.3 (inchangé en C11).
Nikolai Ruhe
27

Extrait du document officiel de justification C

Les termes comportement non spécifié, comportement non défini et comportement défini par l' implémentation sont utilisés pour classer le résultat de l'écriture de programmes dont les propriétés ne décrivent pas, ou ne peuvent pas, complètement les propriétés de la norme. Le but de l'adoption de cette catégorisation est de permettre une certaine variété parmi les implémentations qui permet à la qualité de l'implémentation d'être une force active sur le marché ainsi que de permettre certaines extensions populaires, sans supprimer le cachet de conformité à la norme. L'annexe F du catalogue standard répertorie les comportements qui entrent dans l'une de ces trois catégories.

Un comportement non spécifié donne au réalisateur une certaine latitude dans la traduction des programmes. Cette latitude ne va pas jusqu'à ne pas traduire le programme.

Un comportement indéfini donne à l'implémenteur une licence pour ne pas détecter certaines erreurs de programme difficiles à diagnostiquer. Il identifie également les domaines d'extension linguistique possible: l'implémenteur peut étendre le langage en fournissant une définition du comportement officiellement indéfini.

Le comportement défini par l'implémentation donne à l'implémenteur la liberté de choisir l'approche appropriée, mais nécessite que ce choix soit expliqué à l'utilisateur. Les comportements désignés comme définis par l'implémentation sont généralement ceux dans lesquels un utilisateur peut prendre des décisions de codage significatives sur la base de la définition de l'implémentation. Les implémenteurs doivent tenir compte de ce critère lorsqu'ils décident de l'étendue d'une définition d'implémentation. Comme pour un comportement non spécifié, le simple fait de ne pas traduire la source contenant le comportement défini par l'implémentation n'est pas une réponse adéquate.

Johannes Schaub - litb
la source
3
Les rédacteurs de compilateurs hyper-modernes considèrent également le "comportement indéfini" comme donnant aux rédacteurs du compilateur la licence de supposer que les programmes ne recevront jamais d'entrées qui provoqueraient un comportement indéfini, et de modifier arbitrairement tous les aspects du comportement des programmes lorsqu'ils reçoivent de telles entrées.
supercat
2
Un autre point que je viens de remarquer: C89 n'a pas utilisé le terme "extension" pour décrire les fonctionnalités qui étaient garanties sur certaines implémentations mais pas sur d'autres. Les auteurs de C89 ont reconnu que la majorité des mises en œuvre alors en vigueur traiteraient de manière identique l'arithmétique signée et l'arithmétique non signée, sauf lorsque les résultats étaient utilisés de certaines manières, et un tel traitement s'appliquait même en cas de débordement signé; cependant, ils ne l'ont pas mentionné comme une extension commune à l'annexe J2, ce qui donne à penser qu'ils l'ont considéré comme un état de fait naturel plutôt que comme une extension.
supercat
10

Comportement indéfini et comportement non spécifié en a une brève description.

Leur résumé final:

Pour résumer, un comportement non spécifié est généralement quelque chose dont vous ne devriez pas vous inquiéter, sauf si votre logiciel doit être portable. Inversement, un comportement indéfini est toujours indésirable et ne devrait jamais se produire.

Anders Abel
la source
1
Il existe deux types de compilateurs: ceux qui, sauf indication contraire explicite, interprètent la plupart des formes de comportement indéfini de la norme comme retombant sur des comportements caractéristiques documentés par l'environnement sous-jacent, et ceux qui, par défaut, n'exposent utilement que les comportements que la norme caractérise comme Défini par l'implémentation. Lors de l'utilisation de compilateurs du premier type, de nombreuses choses du premier type peuvent être effectuées efficacement et en toute sécurité en utilisant UB. Les compilateurs du deuxième type ne conviennent à de telles tâches que s'ils offrent des options pour garantir le comportement dans de tels cas.
supercat
8

Historiquement, le comportement défini par l'implémentation et le comportement non défini représentaient des situations dans lesquelles les auteurs de la norme s'attendaient à ce que les personnes qui rédigent des implémentations de qualité utilisent leur jugement pour décider quelles garanties comportementales, le cas échéant, seraient utiles pour les programmes dans le champ d'application prévu s'exécutant sur le cibles prévues. Les besoins du code de numérotation numérique haut de gamme sont assez différents de ceux du code des systèmes de bas niveau, et UB et IDB offrent aux rédacteurs du compilateur la flexibilité nécessaire pour répondre à ces différents besoins. Aucune des deux catégories n'oblige les implémentations à se comporter d'une manière qui soit utile à un but particulier, ou même à quelque fin que ce soit. Cependant, les implémentations de qualité qui prétendent convenir à un usage particulier devraient se comporter d'une manière convenant à cet objectif.si la norme l'exige ou non .

La seule différence entre le comportement défini par l'implémentation et le comportement indéfini est que le premier requiert que les implémentations définissent et documentent un comportement cohérent même dans les cas où rien de l'implémentation ne pourrait être utile . La ligne de démarcation entre eux n'est pas de savoir s'il serait généralement utile pour les implémentations de définir des comportements (les rédacteurs du compilateur devraient définir les comportements utiles lorsque cela est pratique, que la norme les y oblige ou non), mais s'il peut y avoir des implémentations où la définition d'un comportement serait simultanément coûteuse. et inutile . Un jugement selon lequel de telles implémentations pourraient exister n'implique en aucune façon, forme ou forme, un jugement sur l'utilité de prendre en charge un comportement défini sur d'autres plates-formes.

Malheureusement, depuis le milieu des années 1990, les rédacteurs de compilateurs ont commencé à interpréter le manque de mandats comportementaux comme un jugement selon lequel les garanties comportementales ne valent pas le coût, même dans les domaines d'application où elles sont vitales, et même sur les systèmes où elles ne coûtent pratiquement rien. Au lieu de traiter UB comme une invitation à exercer un jugement raisonnable, les rédacteurs du compilateur ont commencé à le traiter comme une excuse pour ne pas le faire.

Par exemple, étant donné le code suivant:

int scaled_velocity(int v, unsigned char pow)
{
  if (v > 250)
    v = 250;
  if (v < -250)
    v = -250;
  return v << pow;
}

une mise en œuvre à deux compléments n'aurait aucun effort à déployer pour traiter l'expression v << powcomme un changement à deux compléments, sans égard au caractère vpositif ou négatif.

Cependant, la philosophie préférée de certains des auteurs de compilateurs actuels suggérerait que, comme vil ne peut être négatif que si le programme va s'engager dans un comportement indéfini, il n'y a aucune raison pour que le programme écrête la plage négative de v. Même si le décalage à gauche des valeurs négatives était pris en charge sur chaque compilateur d'importance et qu'une grande partie du code existant repose sur ce comportement, la philosophie moderne interpréterait le fait que la norme dit que les valeurs négatives à gauche sont UB comme ce qui implique que les rédacteurs du compilateur devraient se sentir libres de l'ignorer.

supercat
la source
Mais gérer un comportement indéfini d'une manière agréable n'est pas gratuit. La raison pour laquelle les compilateurs modernes présentent un comportement aussi bizarre dans certains cas d'UB est qu'ils optimisent sans relâche, et pour faire le meilleur travail à ce sujet, ils doivent être en mesure de supposer que UB ne se produit jamais.
Tom Swirly
Mais le fait qu'il y <<ait UB sur des nombres négatifs est un petit piège désagréable et je suis heureux de m'en souvenir!
Tom Swirly
1
@TomSwirly: Malheureusement, les rédacteurs du compilateur ne se soucient pas que le fait d'offrir des garanties comportementales lâches au-delà de celles prescrites par la norme puisse souvent permettre une augmentation de vitesse massive par rapport à exiger que le code évite à tout prix tout ce qui n'est pas défini par la norme. Si un programmeur ne se soucie pas de savoir si i+j>krenvoie 1 ou 0 dans les cas où l'addition déborde, à condition qu'il n'ait pas d'autres effets secondaires , un compilateur peut être en mesure de faire des optimisations massives qui ne seraient pas possibles si le programmeur écrivait le code comme (int)((unsigned)i+j) > k.
supercat
1
@TomSwirly: Pour eux, si le compilateur X peut prendre un programme strictement conforme pour effectuer une tâche T et produire un exécutable 5% plus efficace que le compilateur Y ne donnerait avec ce même programme, cela signifie que X est meilleur, même si Y pourrait générer du code qui a fait la même tâche trois fois plus efficacement étant donné un programme qui exploite les comportements que Y garantit mais pas X.
supercat
6

Norme n3337 C du § 1.3.10 du comportement défini par l' implémentation

comportement, pour une construction de programme bien formée et des données correctes, qui dépend de l'implémentation et que chaque document d'implémentation

Parfois, C ++ Standard n'impose pas de comportement particulier à certaines constructions mais dit à la place qu'un comportement particulier et bien défini doit être choisi et décrit par une implémentation particulière (version de la bibliothèque). Ainsi, l'utilisateur peut toujours savoir exactement comment le programme se comportera même si Standard ne le décrit pas.


Norme C ++ n3337 § 1.3.24 comportement indéfini

comportement pour lequel la présente Norme internationale n'impose aucune exigence [Remarque: Un comportement indéfini peut être attendu lorsque cette Norme internationale omet toute définition explicite de comportement ou lorsqu'un programme utilise une construction ou des données erronées. Le comportement non défini autorisé va de l'ignorance complète de la situation avec des résultats imprévisibles, au comportement pendant la traduction ou l'exécution du programme d'une manière documentée caractéristique de l'environnement (avec ou sans émission d'un message de diagnostic), à la fin d'une traduction ou d'une exécution (avec l'émission d'un message de diagnostic). De nombreuses constructions de programme erronées n'engendrent pas de comportement indéfini; ils doivent être diagnostiqués. - note de fin]

Lorsque le programme rencontre une construction qui n'est pas définie selon la norme C ++, il est autorisé à faire ce qu'il veut faire (peut-être m'envoyer un e-mail ou peut-être vous envoyer un e-mail ou peut-être ignorer complètement le code).


Norme C ++ n3337 § 1.3.25 comportement non spécifié

comportement, pour une construction de programme bien formée et des données correctes, qui dépend de l'implémentation [Remarque: L'implémentation n'est pas requise pour documenter quel comportement se produit. L'éventail des comportements possibles est généralement défini par la présente Norme internationale. - note de fin]

C ++ Standard n'impose pas de comportement particulier à certaines constructions mais dit à la place qu'un comportement particulier et bien défini doit être choisi (le bot n'est pas nécessairement décrit ) par une implémentation particulière (version de la bibliothèque). Ainsi, dans le cas où aucune description n'a été fournie, il peut être difficile pour l'utilisateur de savoir exactement comment le programme se comportera.

4pie0
la source
6

Mise en œuvre définie-

Les implémenteurs souhaitent, devraient être bien documentés, la norme donne des choix mais sûr de compiler

Non spécifié -

Identique à la définition de l'implémentation mais non documentée

Indéfini-

Tout peut arriver, prenez-en soin.

Suraj K Thomas
la source
2
Je pense qu'il est important de noter que la signification pratique de «non défini» a changé au cours des dernières années. Auparavant uint32_t s;, étant donné cela , évaluer 1u<<squand sest 33 pourrait peut-être donner 0 ou peut-être 2, mais ne rien faire de plus farfelu. Les compilateurs plus récents, cependant, l'évaluation 1u<<speuvent amener un compilateur à déterminer que, parce qu'il sdevait avoir été inférieur à 32 auparavant, tout code avant ou après cette expression qui ne serait pertinent que s'il savait été supérieur ou égal à 32 peut être omis.
supercat