web-dev-qa-db-fra.com

Quelle est la raison pour laquelle la soustraction de deux pointeurs n'est pas liée au même comportement non défini du tableau?

Selon le brouillon C++ expr.add lorsque vous soustrayez des pointeurs du même type, mais n'appartenant pas au même tableau, le comportement n'est pas défini (c'est moi qui souligne):

Lorsque deux expressions de pointeur P et Q sont soustraites, le type du résultat est un type intégral signé défini par l'implémentation; ce type doit être le même type que celui défini comme std :: ptrdiff_t dans l'en-tête ([support.types]).

  • Si P et Q sont tous deux évalués à des valeurs de pointeur nul, le résultat est 0. (5.2)
  • Sinon, si P et Q pointent respectivement sur les éléments x [i] et x [j] du même objet tableau x, l'expression P - Q a la valeur i − j.

  • Sinon, le comportement n'est pas défini. [Remarque: si la valeur i − j n'est pas dans la plage de valeurs représentables de type std :: ptrdiff_t, le le comportement n'est pas défini. - note de fin]

Quelle est la raison pour laquelle un tel comportement n'est pas défini au lieu, par exemple, d'être défini par la mise en œuvre?

16

Parlant plus académiquement: les pointeurs ne sont pas des nombres. Ce sont des pointeurs.

Il est vrai qu'un pointeur sur votre système est implémenté comme une représentation numérique d'une représentation de type adresse d'un emplacement dans une sorte abstraite de mémoire (probablement un espace mémoire virtuel par processus).

Mais C++ s'en fiche. C++ veut que vous pensiez aux pointeurs comme des post-its, comme des signets, vers des objets spécifiques. Les valeurs d'adresse numériques ne sont qu'un effet secondaire. L'arithmétique uniquement qui a du sens sur un pointeur est en avant et en arrière à travers un tableau d'objets; rien d'autre n'a de sens philosophique.

Cela peut sembler assez mystérieux et inutile, mais c'est en fait délibéré et utile. C++ ne veut pas contraindre les implémentations à donner plus de sens aux propriétés informatiques pratiques de bas niveau qu'il ne peut pas contrôler. Et, comme il n'y a aucune raison de le faire (pourquoi voudriez-vous faire cela?), Cela indique simplement que le résultat n'est pas défini.

En pratique, vous pouvez constater que votre soustraction fonctionne. Cependant, les compilateurs sont extrêmement compliqués et font un grand usage des règles de la norme afin de générer le code le plus rapide possible; cela peut et fera souvent que votre programme semble faire des choses étranges lorsque vous enfreignez les règles. Ne soyez pas trop surpris si votre opération arithmétique de pointeur est altérée lorsque le compilateur suppose que la valeur d'origine et le résultat se réfèrent au même tableau - une supposition que vous avez violée.

Comme indiqué par certains dans les commentaires, à moins que la valeur résultante ait un sens ou soit utilisable d'une manière ou d'une autre, il est inutile de définir le comportement.

Une étude a été réalisée pour le langage C afin de répondre aux questions liées à la provenance du pointeur (et avec l'intention de proposer des modifications de libellé à la spécification C.) et l'une des questions était:

Peut-on faire un décalage utilisable entre deux objets alloués séparément par soustraction inter-objets (en utilisant soit un pointeur soit une arithmétique entière), pour créer un pointeur utilisable vers le second en ajoutant le décalage au premier? (source)

La conclusion des auteurs de l'étude a été publiée dans un article intitulé: Exploring C Semantics and Pointer Provenance and in respect to cette question particulière, la réponse était:

Arithmétique des pointeurs inter-objets Le premier exemple de cette section reposait sur la supposition (puis la vérification) du décalage entre deux allocations. Que se passe-t-il si l'on calcule à la place le décalage, avec soustraction du pointeur; cela devrait-il permettre de se déplacer entre les objets, comme ci-dessous?

// pointer_offset_from_ptr_subtraction_global_xy.c
#include <stdio.h>
#include <string.h>
#include <stddef.h>

int x=1, y=2;
int main() {
    int *p = &x;
    int *q = &y;
    ptrdiff_t offset = q - p;
    int *r = p + offset;
    if (memcmp(&r, &q, sizeof(r)) == 0) {
        *r = 11; // is this free of UB?
        printf("y=%d *q=%d *r=%d\n",y,*q,*r);
    }
}

Dans ISO C11, le q-p est UB (comme une soustraction de pointeur entre des pointeurs vers différents objets, qui dans certaines exécutions de machine abstraite ne sont pas liées au passé). Dans une variante sémantique qui permet la construction de plusieurs pointeurs passés, il faudrait choisir si le *r=11 l'accès est UB ou non. La sémantique de provenance de base l'interdira, car r conservera la provenance de l'allocation x, mais son adresse n'est pas interdite pour cela. C'est probablement la sémantique la plus souhaitable: nous avons trouvé très peu d'exemples d'idiomes qui utilisent intentionnellement l'arithmétique du pointeur inter-objets, et la liberté que l'interdire donne à l'analyse et à l'optimisation des alias semble significative.

Cette étude a été reprise par la communauté C++, résumée et envoyée au WG21 (The C++ Standards Committee) pour commentaires.

Point pertinent du résumé :

La différence de pointeur n'est définie que pour les pointeurs de même provenance et dans le même tableau.

Donc, ils ont décidé de ne pas le définir pour l'instant.

Notez qu'il existe un groupe d'étude SG12 au sein du Comité des normes C++ pour étudier Comportement et vulnérabilités non définis. Ce groupe effectue une revue systématique pour cataloguer les cas de vulnérabilités et de comportements indéfinis/non spécifiés dans la norme, et recommande un ensemble cohérent de changements pour définir et/ou spécifier le comportement. Vous pouvez suivre les procédures de ce groupe pour voir s'il y aura des changements à l'avenir dans les comportements qui sont actuellement indéfinis ou non spécifiés.

8
P.W

Voir d'abord cette question mentionnée dans les commentaires pour savoir pourquoi elle n'est pas bien définie. La réponse donnée de manière concise est que l'arithmétique des pointeurs arbitraires n'est pas possible dans les modèles de mémoire segmentée utilisés par certains systèmes (maintenant archaïques?).

Quelle est la raison pour laquelle un tel comportement n'est pas défini au lieu, par exemple, de la mise en œuvre définie?

Chaque fois que la norme spécifie quelque chose comme un comportement non défini, elle peut généralement être spécifiée simplement pour être définie par l'implémentation. Alors, pourquoi spécifier quoi que ce soit comme non défini?

Eh bien, un comportement indéfini est plus indulgent. En particulier, étant autorisé à supposer qu'il n'y a pas de comportement indéfini, un compilateur peut effectuer des optimisations qui briseraient le programme si les hypothèses n'étaient pas correctes. Ainsi, une raison de spécifier un comportement non défini est l'optimisation.

Prenons la fonction fun(int* arr1, int* arr2) qui prend deux pointeurs comme arguments. Ces pointeurs pourraient pointer vers le même tableau, ou non. Disons que la fonction parcourt l'un des tableaux pointés (arr1 + n) Et doit comparer chaque position à l'autre pointeur pour l'égalité ((arr1 + n) != arr2) À chaque itération. Par exemple, pour vous assurer que l'objet pointé n'est pas remplacé.

Disons que nous appelons la fonction comme ceci: fun(array1, array2). Le compilateur sait que (array1 + n) != array2, Car sinon le comportement n'est pas défini. Par conséquent, si l'appel de fonction est développé en ligne, le compilateur peut supprimer la vérification redondante (arr1 + n) != arr2 Qui est toujours vraie. Si l'arithmétique du pointeur à travers les limites du tableau était bien définie (ou même implémentée), alors (array1 + n) == array2 Pourrait être vrai avec un peu de n, et cette optimisation serait impossible - à moins que le compilateur ne prouve que (array1 + n) != array2 Est valable pour toutes les valeurs possibles de n qui peuvent parfois être plus difficiles à prouver.


L'arithmétique du pointeur entre les membres d'une classe peut être implémentée même dans les modèles de mémoire segmentée. Il en va de même pour itérer sur les limites d'un sous-tableau. Il existe des cas d'utilisation où ceux-ci pourraient être très utiles, mais ceux-ci sont techniquement UB.

Un argument pour UB dans ces cas est plus de possibilités pour l'optimisation UB. Vous n'avez pas nécessairement besoin de convenir qu'il s'agit d'un argument suffisant.

5
eerorika