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?
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?
Réponses:
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 p
pointe 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.
int f(){int a; return a;}
: la valeur de a
peut changer entre les appels de fonction.
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.
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.
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 int
qui 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.
fun(fun1(), fun2());
le comportement n'est-il pas "implementation defined"
? Le compilateur doit choisir l'un ou l'autre cours, après tout?
"I am gonna evaluate these arguments left-to-right and the next function's arguments are evaluated right-to-left"
je comprends que cela can
se produise. Est-ce vraiment le cas avec les compilateurs que nous utilisons de nos jours?
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.
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.
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 << pow
comme un changement à deux compléments, sans égard au caractère v
positif ou négatif.
Cependant, la philosophie préférée de certains des auteurs de compilateurs actuels suggérerait que, comme v
il 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.
<<
ait UB sur des nombres négatifs est un petit piège désagréable et je suis heureux de m'en souvenir!
i+j>k
renvoie 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
.
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.
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.
uint32_t s;
, étant donné cela , évaluer 1u<<s
quand s
est 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<<s
peuvent amener un compilateur à déterminer que, parce qu'il s
devait avoir été inférieur à 32 auparavant, tout code avant ou après cette expression qui ne serait pertinent que s'il s
avait été supérieur ou égal à 32 peut être omis.