Cet article Stack Overflow répertorie une liste assez complète de situations dans lesquelles la spécification de langage C / C ++ déclare être un "comportement non défini". Cependant, je veux comprendre pourquoi d'autres langages modernes, tels que C # ou Java, n'ont pas le concept de «comportement indéfini». Cela signifie-t-il que le concepteur du compilateur peut contrôler tous les scénarios possibles (C # et Java) ou non (C et C ++)?
50
nullptr
) non on se donnait la peine de définir le comportement en écrivant et / ou en adoptant une spécification proposée ". : cRéponses:
Un comportement indéfini est l'une de ces choses qui ont été reconnues comme une très mauvaise idée seulement rétrospectivement.
Les premiers compilateurs étaient de grandes réalisations et accueillaient avec enthousiasme les améliorations apportées à la programmation alternative: langage machine ou programmation en langage assembleur. Les problèmes rencontrés étaient bien connus et des langages de haut niveau ont été spécialement inventés pour résoudre ces problèmes connus. (L'enthousiasme à l'époque était si grand que les HLL ont parfois été saluées comme "la fin de la programmation" - comme si désormais nous n'aurions plus qu'à écrire de manière triviale ce que nous voulions et que le compilateur ferait tout le travail réel.)
Ce n'est que plus tard que nous avons réalisé les problèmes plus récents posés par la nouvelle approche. Être distant de la machine sur laquelle le code est exécuté signifie qu'il est plus probable que des choses ne fassent pas ce que nous attendons d'eux. Par exemple, l'allocation d'une variable laisserait généralement la valeur initiale indéfinie; cela n'était pas considéré comme un problème, car vous n'alloueriez pas de variable si vous ne vouliez pas en conserver une valeur, n'est-ce pas? Ce n'était sûrement pas trop attendre que les programmeurs professionnels n'oublient pas d'attribuer la valeur initiale, n'est-ce pas?
Il s'est avéré qu'avec les bases de code plus larges et les structures plus complexes rendues possibles par des systèmes de programmation plus puissants, de nombreux programmeurs commettaient effectivement de telles omissions de temps en temps, et le comportement indéfini qui en résultait devenait un problème majeur. Même aujourd'hui, la majorité des fuites de sécurité mineures à horribles sont le résultat d'un comportement indéfini sous une forme ou une autre. (La raison en est que, habituellement, un comportement non défini est en fait très défini par les éléments du niveau informatique inférieur, et les attaquants qui comprennent ce niveau peuvent utiliser cette marge de manœuvre pour faire en sorte qu'un programme ne fasse pas que des choses inattendues, mais ils ont l' intention.)
Depuis que nous en avons pris conscience, il y a eu une volonté générale de bannir les comportements indéfinis des langages de haut niveau, et Java a été particulièrement complet à ce sujet (ce qui était relativement facile, car il était conçu pour fonctionner de toute façon sur une machine virtuelle spécifiquement conçue à cet effet). Les langages plus anciens tels que C ne peuvent pas être facilement installés comme cela sans perdre la compatibilité avec la quantité énorme de code existant.
Edit: Comme indiqué, l'efficacité est une autre raison. Un comportement indéfini signifie que les rédacteurs de compilateur disposent de beaucoup de marge de manœuvre pour exploiter l'architecture cible, de sorte que chaque implémentation bénéficie de la mise en œuvre la plus rapide possible de chaque fonctionnalité. C'était plus important sur les machines sous-alimentées d'hier qu'aujourd'hui, quand le salaire d'un programmeur est souvent le goulot d'étranglement pour le développement de logiciels.
la source
int32_t add(int32_t x, int32_t y)
) en C ++. Les arguments habituels autour de celui-ci sont liés à l'efficacité, mais sont souvent entrecoupés d'arguments de portabilité (comme dans "Écrivez une fois, exécutez ... sur la plate-forme où vous l'avez écrit ... et nulle part ailleurs ;-)"). En gros, un argument pourrait donc être le suivant: certaines choses ne sont pas définies car vous ne savez pas si vous êtes sur un microcontrôleur 16 bits ou sur un serveur 64 bits (un serveur faible, mais toujours un argument)Fondamentalement, les concepteurs de Java et de langages similaires ne souhaitaient pas un comportement indéfini dans leur langage. C’était un compromis - permettre à un comportement indéfini d’améliorer ses performances, mais les concepteurs de langage ont privilégié la sécurité et la prévisibilité.
Par exemple, si vous allouez un tableau en C, les données ne sont pas définies. En Java, tous les octets doivent être initialisés à 0 (ou à une autre valeur spécifiée). Cela signifie que le moteur d'exécution doit passer par-dessus le tableau (une opération O (n)), tandis que C peut effectuer l'allocation en un instant. Donc C sera toujours plus rapide pour de telles opérations.
Si le code utilisant le tableau doit le peupler avant la lecture, il s’agit là d’un effort inutile pour Java. Mais dans le cas où le code est lu en premier, vous obtenez des résultats prévisibles en Java mais des résultats imprévisibles en C.
la source
valgrind
, qui indiquerait exactement où la valeur non initialisée a été utilisée. Vous ne pouvez pas utiliservalgrind
de code java car le runtime effectue l'initialisation, ce qui rendvalgrind
les contrôles inutiles.Un comportement non défini permet une optimisation significative, en laissant au compilateur la latitude nécessaire pour faire quelque chose d’étrange ou d’inattendu (voire même normal) à certaines limites ou dans d’autres conditions.
Voir http://blog.llvm.org/2011/05/what-every-c-programmer-should-know.html
la source
a + b
de compiler l'add b a
instruction native dans chaque situation, plutôt que de demander éventuellement à un compilateur de simuler une autre forme d'arithmétique des entiers signés.HashSet
est merveilleux.<<
pourrait être le cas difficile.x << y
évalue à une valeur valide du typeint32_t
mais nous ne dirons pas laquelle". Cela permet aux implémenteurs d'utiliser la solution rapide, mais n'agit pas comme une fausse condition permettant des optimisations de style de voyage dans le temps car le non déterminisme est contraint à la sortie de cette opération - les spécifications garantissent que la mémoire, les variables volatiles, etc. ne sont pas visiblement affectées par l'évaluation de l'expression. ...Au début de C, il y avait beaucoup de chaos. Différents compilateurs ont traité la langue différemment. Lorsqu'il était intéressant d'écrire une spécification pour le langage, cette spécification devrait être assez rétro-compatible avec le C que les programmeurs utilisaient avec leurs compilateurs. Mais certains de ces détails ne sont pas portables et n'ont pas de sens en général, par exemple en supposant une finalité particulière ou une structure de données particulière. La norme C réserve donc beaucoup de détails en tant que comportement indéfini ou spécifié par la mise en œuvre, ce qui laisse beaucoup de souplesse aux rédacteurs du compilateur. C ++ s'appuie sur C et présente également un comportement indéfini.
Java a essayé d’être un langage beaucoup plus sûr et beaucoup plus simple que C ++. Java définit la sémantique du langage en termes de machine virtuelle complète. Cela laisse peu de place au comportement indéfini, mais crée des exigences qui peuvent être difficiles à implémenter pour une implémentation Java (par exemple, les assignations de référence doivent être atomiques ou le fonctionnement des entiers). Lorsque Java prend en charge des opérations potentiellement non sûres, elles sont généralement vérifiées par la machine virtuelle au moment de l’exécution (par exemple, certains conversions).
la source
this
nul?" Vérifie il y a quelque temps, au motifthis
qu'êtrenullptr
est UB, et ne peut donc jamais se produire.)Les langages JVM et .NET sont simples:
Il y a de bons points pour les choix cependant:
Lorsque des trappes d'échappement sont fournies, celles-ci invitent à un comportement non défini complet. Mais au moins, elles ne sont généralement utilisées que sur quelques tronçons très courts, qui sont donc plus faciles à vérifier manuellement.
la source
unsafe
mot-clé ou attributsSystem.Runtime.InteropServices
). En gardant ce matériel pour les quelques programmeurs qui savent comment déboguer des documents non gérés et aussi peu que pratique, nous réduisons les problèmes. Plus de 10 ans se sont écoulés depuis le dernier marteau non sûr lié à la performance, mais il faut parfois le faire parce qu'il n'y a littéralement aucune autre solution.Java et C # se caractérisent par un fournisseur dominant, du moins tôt dans leur développement. (Sun et Microsoft respectivement). C et C ++ sont différents; ils ont eu plusieurs implémentations concurrentes depuis le début. C a particulièrement fonctionné sur des plates-formes matérielles exotiques, aussi. En conséquence, il y avait une variation entre les implémentations. Les comités ISO qui ont normalisé le C et le C ++ pourraient se mettre d’accord sur un grand dénominateur commun, mais aux confins où les mises en œuvre diffèrent, les normes laissent une marge de manœuvre pour la mise en oeuvre.
Cela est également dû au fait que le choix d’un comportement peut être coûteux pour des architectures matérielles qui privilégient un autre choix - l’endianisme est le choix évident.
la source
La vraie raison se résume à une différence fondamentale d'intention entre C et C ++, d'une part, et Java et C # (pour quelques exemples seulement), d'autre part. Pour des raisons historiques, une grande partie de la discussion ici porte sur le C plutôt que sur le C ++, mais (comme vous le savez probablement déjà), le C ++ est un descendant assez direct du C, aussi ce qu'il dit à propos de C s'applique également au C ++.
Bien qu'ils soient en grande partie oubliés (et leur existence parfois même niée), les toutes premières versions d'UNIX ont été écrites en langage assembleur. Une grande partie (sinon uniquement) de l'objectif initial de C était de transférer UNIX du langage d'assemblage à un langage de niveau supérieur. Une partie de l’intention était d’écrire le plus possible le système d’exploitation dans un langage de niveau supérieur - ou de le regarder dans l’autre sens, afin de minimiser la quantité qui devait être écrite en langage assembleur.
Pour ce faire, C devait fournir à peu près le même niveau d'accès au matériel que le langage d'assemblage. Le PDP-11 (pour un exemple) a mappé des registres d'E / S à des adresses spécifiques. Par exemple, vous liriez un emplacement de mémoire pour vérifier si une touche avait été enfoncée sur la console système. Un bit a été placé à cet endroit lorsqu'il y avait des données en attente de lecture. Vous liriez ensuite un octet depuis un autre emplacement spécifié pour récupérer le code ASCII de la touche sur laquelle vous avez appuyé.
De même, si vous souhaitez imprimer des données, vous devez vérifier un autre emplacement spécifié et, lorsque le périphérique de sortie est prêt, vous écrivez vos données dans un autre emplacement spécifié.
Pour prendre en charge l’écriture de pilotes pour de tels périphériques, C vous permettait de spécifier un emplacement quelconque en utilisant un type entier, de le convertir en pointeur et de lire ou d’écrire cet emplacement en mémoire.
Bien sûr, cela pose un problème assez sérieux: toutes les machines sur terre n’ont pas leur mémoire identique à celle d’un PDP-11 du début des années 1970. Ainsi, lorsque vous prenez cet entier, que vous le convertissez en un pointeur, puis que vous lisez ou écrivez via ce pointeur, personne ne peut fournir de garantie raisonnable quant à ce que vous allez obtenir. Juste pour un exemple évident, la lecture et l’écriture peuvent mapper des registres séparés dans le matériel. Ainsi, contrairement à la mémoire normale, si vous écrivez quelque chose, puis essayez de le relire, ce que vous lisez peut ne pas correspondre à ce que vous avez écrit.
Je peux voir quelques possibilités qui nous laissent:
Parmi ceux-ci, 1 semble suffisamment absurde pour que nous n’ayions pas besoin de poursuivre la discussion. 2 consiste essentiellement à jeter l’intention fondamentale de la langue. Cela laisse la troisième option essentiellement la seule option qu’ils pourraient raisonnablement envisager.
Un autre point qui revient assez souvent est la taille des types entiers. C prend la "position" qui
int
devrait être la taille naturelle suggérée par l'architecture. Donc, si je programme un VAX 32 bits, celaint
devrait probablement être 32 bits, mais si je programme un Univac 36 bits, celaint
devrait probablement être 36 bits (et ainsi de suite). Il n'est probablement pas raisonnable (et même impossible) d'écrire un système d'exploitation pour un ordinateur 36 bits en utilisant uniquement des types dont la taille est garantie être un multiple de 8 bits. Je suis peut-être superficiel, mais il me semble que si j'écrivais un système d'exploitation pour une machine 36 bits, je préférerais probablement utiliser un langage prenant en charge le type 36 bits.Du point de vue de la langue, cela conduit à un comportement encore plus indéfini. Si je prends la plus grande valeur qui puisse tenir dans 32 bits, qu’arrivera-t-il si j’ajoute 1? Sur un matériel 32 bits typique, il va basculer (ou éventuellement renvoyer une sorte de défaillance matérielle). D'un autre côté, s'il fonctionne sur du matériel 36 bits, il vous suffira ... d'en ajouter un. Si le langage prend en charge l’écriture de systèmes d’exploitation, vous ne pouvez garantir aucun comportement: vous devez autoriser à la fois la taille des types et le comportement du débordement à varier.
Java et C # peuvent ignorer tout cela. Ils ne sont pas conçus pour prendre en charge l'écriture de systèmes d'exploitation. Avec eux, vous avez plusieurs choix. L’une consiste à faire en sorte que le matériel supporte ce qu’ils exigent - car ils exigent des types de 8, 16, 32 et 64 bits, il suffit de créer du matériel qui prend en charge ces tailles. L'autre possibilité évidente est que la langue ne s'exécute que sur d'autres logiciels fournissant l'environnement qu'ils souhaitent, quel que soit le matériel sous-jacent.
Dans la plupart des cas, ce n'est pas vraiment un choix. Au contraire, de nombreuses implémentations font un peu des deux. Vous exécutez normalement Java sur une machine virtuelle Java s'exécutant sur un système d'exploitation. Le plus souvent, le système d'exploitation est écrit en C et la JVM en C ++. Si la machine virtuelle Java s'exécute sur un processeur ARM, il est fort probable que le processeur intègre les extensions Jazelle d'ARM, afin d'adapter le matériel aux besoins de Java. Il est donc inutile de faire des logiciels et le code Java s'exécute plus rapidement (ou moins). lentement, quand même).
Sommaire
C et C ++ ont un comportement indéfini, car personne n'a défini d'alternative acceptable lui permettant de faire ce qu'il est censé faire. C # et Java adoptent une approche différente, mais cette approche correspond mal (voire pas du tout) aux objectifs de C et C ++. En particulier, ni l'un ni l'autre ne semble constituer un moyen raisonnable d'écrire un logiciel système (tel qu'un système d'exploitation) sur le matériel le plus arbitrairement choisi. Les deux dépendent généralement des fonctionnalités fournies par le logiciel système existant (généralement écrit en C ou C ++) pour effectuer leur travail.
la source
Les auteurs de la norme C s'attendaient à ce que leurs lecteurs reconnaissent quelque chose qui, à leur avis, était évident, et qui était mentionné dans la justification publiée, mais n'a pas dit carrément: le Comité ne devrait pas avoir besoin de commander des rédacteurs de compilateur pour répondre aux besoins de leurs clients, puisque les clients devraient savoir mieux que le Comité quels sont leurs besoins. S'il est évident que les compilateurs de certains types de plates-formes sont censés traiter une construction d'une certaine manière, personne ne devrait se soucier de savoir si la norme dit que cette construction appelle le comportement non défini. L'échec de la norme à imposer aux compilateurs conformes de traiter utilement un morceau de code ne signifie nullement que les programmeurs devraient être disposés à acheter des compilateurs qui ne le font pas.
Cette approche de la conception linguistique fonctionne très bien dans un monde où les rédacteurs de compilateurs doivent vendre leurs produits à des clients payants. Il s'effondre complètement dans un monde où les rédacteurs de compilateurs sont isolés des effets du marché. Il est douteux que les conditions de marché appropriées existeront jamais pour orienter une langue de la même façon que celle qui était devenue populaire dans les années 90, et encore plus de doute qu'un concepteur de langage sain d'esprit voudrait compter sur de telles conditions de marché.
la source
C ++ et c ont tous deux des normes descriptives (les versions ISO, en tout cas).
Ce qui n’existe que pour expliquer le fonctionnement des langues et pour fournir une référence unique sur la langue. Généralement, les éditeurs de compilateurs et les rédacteurs de bibliothèques ouvrent la voie et certaines suggestions sont incluses dans la norme ISO principale.
Java et C # (ou Visual C #, ce que je suppose vous entendez dire) ont des normes prescriptives . Ils vous disent ce qui est dans la langue définitivement à l'avance, comment cela fonctionne et ce qui est considéré comme un comportement autorisé.
Plus important encore, Java a en réalité une "implémentation de référence" dans Open-JDK. (Je pense que Roslyn compte comme implémentation de référence Visual C #, mais n'a pas pu trouver de source pour cela.)
Dans le cas de Java, s'il y a une ambiguïté dans le standard et qu'Open-JDK le fasse d'une certaine manière. La manière dont Open-JDK le fait est la norme.
la source
Un comportement non défini permet au compilateur de générer un code très efficace sur une variété d'architectures. La réponse d'Erik mentionne l'optimisation, mais cela va au-delà.
Par exemple, les débordements signés constituent un comportement non défini en C. En pratique, le compilateur était censé générer un simple code d'opération d'addition signé à exécuter par la CPU.
Cela a permis à C de très bonnes performances et de produire un code très compact sur la plupart des architectures. Si le standard avait spécifié que les entiers signés devaient déborder d'une certaine manière, les CPU qui se comportaient différemment auraient eu besoin de beaucoup plus de code pour générer un simple ajout signé.
C’est ce qui explique en grande partie le comportement indéfini en C et explique pourquoi des facteurs tels que la taille des
int
fichiers varient d’un système à l’autre.Int
dépend de l'architecture et est généralement sélectionné pour être le type de données le plus rapide et le plus efficace, supérieur à achar
.À l'époque où C était nouveau, ces considérations étaient importantes. Les ordinateurs étaient moins puissants, leur vitesse de traitement et leur mémoire étaient souvent limitées. C était utilisé là où les performances importaient vraiment, et les développeurs devaient comprendre comment les ordinateurs fonctionnaient assez bien pour savoir quels seraient ces comportements non définis sur leurs systèmes particuliers.
Des langages ultérieurs tels que Java et C # ont préféré éliminer les comportements indéfinis par rapport aux performances brutes.
la source
En un sens, Java l’a également. Supposons que vous ayez donné un comparateur incorrect à Arrays.sort. Il peut jeter une exception s'il le détecte. Sinon, il va trier un tableau d'une manière qui n'est pas garantie d'être particulière.
De même, si vous modifiez une variable à partir de plusieurs threads, les résultats sont également imprévisibles.
C ++ est juste allé plus loin pour créer plus de situations non définies (ou plutôt java a décidé de définir plus d'opérations) et pour avoir un nom.
la source
a
serait un comportement indéfini si vous pouviez en obtenir 51 ou 73, mais si vous ne pouvez obtenir que 53 ou 71, il est bien défini.