6 votes

La différence de pointeur entre deux tableaux est-elle définie par des implémentations spécifiques ?

Selon la norme C :

Lorsque deux pointeurs sont soustraits, ils doivent tous deux pointer vers des éléments de l'élément même objet tableau, ou un élément après le dernier élément de l'objet tableau. (sect. 6.5.6 1173)

(Note : ne croyez pas que je connaisse bien les normes ou l'UB, il se trouve juste que j'ai découvert celle-ci).

  1. Je comprends que dans presque tous les cas, prendre la différence des pointeurs dans deux tableaux différents serait de toute façon une mauvaise idée.
  2. Je sais aussi que sur certaines architectures ("machine segmentée" comme je l'ai lu quelque part), il y a de bonnes raisons pour que le comportement soit indéfini.

Maintenant, d'autre part

  1. Elle peut être utile dans certains cas particuliers. Par exemple, dans ce poste cela permettrait d'utiliser une interface de bibliothèque avec différents tableaux, au lieu de tout copier dans un tableau qui sera divisé juste après.
  2. Il semble que sur les architectures "ordinaires", la manière de penser "tous les objets sont stockés dans un grand tableau commençant à environ 0 et finissant à environ la taille de la mémoire" est une description raisonnable de la mémoire. Lorsque vous examinez réellement les différences entre les pointeurs de différents tableaux, vous obtenez des résultats raisonnables.

D'où ma question : d'après l'expérience, il semble que sur certaines architectures (par exemple x86-64), la différence de pointeur entre deux tableaux donne des résultats sensibles et reproductibles. Et cela semble correspondre raisonnablement bien au matériel de ces architectures. Donc, est-ce que certaines implémentations assurent réellement un comportement spécifique ?

Par exemple, existe-t-il une implémentation dans la nature qui garantisse que a y b être char* tenemos a + (reinterpret_cast<std::ptrdiff_t>(b)-reinterpret_cast<std::ptrdiff_t>(a)) == b ?

6voto

DevSolar Points 18897

Pourquoi le rendre UB, et non pas défini par la mise en œuvre ? (Bien sûr, pour certaines architectures, la définition de l'implémentation spécifiera qu'il s'agit d'UB).

Ce n'est pas comme ça que ça marche.

Si quelque chose est documenté comme étant "défini par l'implémentation" par la norme, alors toute implémentation conforme est censée définir un comportement pour ce cas, et le documenter. Le laisser indéfini n'est pas une option.

Comme l'étiquetage de la différence de pointeur entre des tableaux non liés "définis par l'implémentation" laisserait par ex. segmenté o Harvard architectures sans possibilité d'avoir une mise en œuvre totalement conforme, ce cas reste indéfini par la norme.

Les implémentations pourraient offrir un comportement défini comme une extension non standard. Mais tout programme utilisant une telle extension ne serait plus strictement conforme et ne serait pas portable.

5voto

Antti Haapala Points 11542

Toute mise en œuvre est gratuit pour documenter un comportement pour lequel la norme n'exige pas qu'il soit documenté - il se situe bien dans les limites de la norme. Le problème avec le comportement défini par l'implémentation dans ce cas est que les implémentations doivent ensuite les documenter soigneusement, et lorsque C a été standardisé, le comité a probablement découvert que les différentes implémentations étaient tellement variables qu'il n'y aurait pas de terrain d'entente raisonnable.


Je ne connais pas de compilateurs qui faire le rendre défini, mais je connaître un compilateur ce qui la garde explicitement indéfinie, même si vous essayez de tricher avec des casts :

Lors du passage d'un pointeur à un entier et inversement le pointeur résultant doit référencer le même objet que le pointeur original, sinon le comportement est indéfini. En d'autres termes, il n'est pas possible d'utiliser l'arithmétique des nombres entiers pour éviter le comportement indéfini de l'arithmétique des pointeurs, comme le prévoient les normes C99 et C11 6.5.6/8.

Je crois un autre compilateur a également le même comportement, bien que, malheureusement, il n'a pas le documenter de manière accessible .

Que ces deux compilateurs font no définir ce serait une bonne raison d'éviter d'en dépendre dans tous les programmes, même s'ils sont compilés avec un autre compilateur qui spécifierait un comportement, car on ne peut jamais être trop sûr du compilateur que l'on devra utiliser dans 5 ans...

4voto

Nicol Bolas Points 133791

Plus vous avez de comportements définis par l'implémentation et dont dépend le code de quelqu'un, moins ce code est portable. Dans ce cas, il y a déjà un moyen défini par l'implémentation de s'en sortir : reinterpret_cast les pointeurs vers des entiers et faites vos calculs là. Ainsi, il est clair pour tout le monde que vous vous appuyez sur un comportement spécifique à l'implémentation (ou du moins, un comportement qui peut ne pas être portable partout).

De plus, si l'environnement d'exécution peut en fait être "tous les objets sont stockés dans un grand tableau commençant à environ 0 et se terminant à environ la taille de la mémoire", ce n'est pas le cas de l'environnement d'exécution. temps de compilation comportement. Au moment de la compilation, vous pouvez obtenir des pointeurs sur des objets et faire de l'arithmétique de pointeur sur eux. Mais traiter ces pointeurs comme de simples adresses en mémoire pourrait permettre à un utilisateur de commencer à indexer les données du compilateur et autres. En faisant de telles choses UB, cela les rend expressément interdites au moment de la compilation (et à la fin de la compilation). reinterpret_cast est explicitement interdit à la compilation).

1voto

Broman Points 5642

Une raison importante pour dire que les choses sont UB est de permettre au compilateur d'effectuer des optimisations. Si vous voulez permettre une telle chose, alors vous supprimez certaines optimisations. Et comme vous le dites, cela n'est utile que dans certains petits cas particuliers. Je dirais que dans la plupart des cas où cela pourrait sembler être une option viable, vous devriez plutôt reconsidérer votre conception.

D'après les commentaires ci-dessous :

Je suis d'accord mais le problème est que si je peux reconsidérer ma conception, je ne peux pas reconsidérer la conception d'autres bibliothèques

Il est très rare que la norme adopte de telles choses. Mais cela s'est produit. C'est la raison pour laquelle int *p = 0 est parfaitement valide, même si p est un pointeur et 0 est un int . Il a été intégré à la norme parce qu'il était couramment utilisé à la place de l'expression plus correcte, à savoir int *p = NULL . Mais en général, cela ne se produit pas, et pour de bonnes raisons.

1voto

John Bode Points 33046

Tout d'abord, j'ai l'impression que nous devons clarifier certains termes, au moins en ce qui concerne C.

De la Projet en ligne C2011 :

  • 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 lesquels la présente Norme internationale n'impose aucune exigence. Le comportement indéfini possible va de l'ignorance totale de la situation avec des résultats imprévisibles, à un comportement pendant la traduction ou la programmation. imprévisibles, à se comporter pendant la traduction ou l'exécution du programme d'une manière documentée et caractéristique de l'environnement (avec ou sans émission d'un avis de conformité). l'environnement (avec ou sans l'émission d'un message de diagnostic), à l'interruption d'une traduction ou d'une exécution (avec l'émission d'un message de diagnostic). ou l'exécution (avec l'émission d'un message de diagnostic).

  • Comportement non spécifié - l'utilisation d'une valeur non spécifiée, ou tout autre comportement pour lequel la présente Norme internationale prévoit deux possibilités ou plus et n'impose pas d'autres exigences quant à celle qui est choisie dans un cas donné. cas. L'ordre dans lequel les arguments d'une fonction sont évalués est un exemple de comportement non spécifié. évalués.

  • Comportement défini par la mise en œuvre - un comportement non spécifié où chaque implémentation documente comment le choix est fait. Un exemple de comportement défini par l'implémentation est la propagation du bit de poids fort lorsqu'un nombre entier signé est décalé vers la droite.

Le point clé ci-dessus est que non spécifié comportement signifie que la définition du langage fournit plusieurs valeurs ou comportements parmi lesquels l'implémentation peut choisir, et qu'il n'y a pas d'autres exigences sur la façon dont ce choix est fait. Un comportement non spécifié devient définie par la mise en œuvre comportement lorsque la mise en œuvre documente la manière dont elle fait ce choix.

Cela signifie qu'il existe des restrictions sur ce qui peut être considéré comme un comportement défini par l'implémentation.

L'autre point essentiel est que indéfini ne signifie pas illégal cela signifie seulement imprévisible . Cela signifie que vous avez annulé la garantie, et que tout ce qui se passe ensuite n'est pas de la responsabilité de l'implémentation du compilateur. Un résultat possible du comportement indéfini est de travailler exactement comme prévu sans effets secondaires désagréables. Ce qui, franchement, est le pire résultat possible, car cela signifie que dès que quelque chose dans le code ou l'environnement change, tout peut exploser sans que vous sachiez pourquoi (j'ai vu ce film plusieurs fois).

Venons-en maintenant à la question qui nous occupe :

Je sais aussi que sur certaines architectures ("machine segmentée" comme je l'ai lu quelque part), il y a de bonnes raisons pour que le comportement soit indéfini.

Et c'est pourquoi c'est indéfini. partout . Il existe encore des architectures dans lesquelles des objets différents peuvent être stockés dans des segments de mémoire différents, et toute différence dans leurs adresses serait sans signification. Il existe tellement de modèles de mémoire et de schémas d'adressage différents que l'on ne peut espérer définir un comportement qui fonctionne de manière cohérente pour tous (ou alors la définition serait si compliquée qu'elle serait difficile à mettre en œuvre).

La philosophie du C est d'être le plus portable possible sur le plus grand nombre d'architectures possible, et pour cela, il impose le moins d'exigences possible à l'implémentation. C'est pourquoi les types arithmétiques standard ( int , float etc.) sont définis par le plage minimale de valeurs qu'ils peuvent représenter avec un précision minimale et non par le nombre de bits qu'ils occupent. C'est pourquoi les pointeurs de différents types peuvent avoir des tailles et des alignements différents.

L'ajout d'un langage qui rendrait certains comportements non définis sur cette liste d'architectures par rapport à non spécifiés sur cette liste d'architectures serait une maux de tête à la fois pour le comité de normalisation et pour divers compilateurs. Cela signifierait ajouter beaucoup de logique de cas spéciaux à des compilateurs comme gcc ce qui pourrait le rendre moins fiable en tant que compilateur.

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