L'incrémentation d'un pointeur vers un tableau dynamique de taille 0 n'est-elle pas définie?

34

AFAIK, bien que nous ne puissions pas créer un tableau de mémoire statique de taille 0, mais nous pouvons le faire avec des tableaux dynamiques:

int a[0]{}; // Compile-time error
int* p = new int[0]; // Is well-defined

Comme je l'ai lu, pagit comme un élément d'un bout à l'autre. Je peux imprimer l'adresse qui ppointe vers.

if(p)
    cout << p << endl;
  • Bien que je sois sûr de nous ne pouvons pas déréférencer ce pointeur (passé-dernier-élément) comme nous ne pouvons pas avec les itérateurs (passé-dernier élément), mais ce dont je ne suis pas sûr est de savoir si l'incrémentation de ce pointeur p? Un comportement non défini (UB) est-il comme avec les itérateurs?

    p++; // UB?
Itachi Uchiwa
la source
4
UB "... Toutes les autres situations (c'est-à-dire les tentatives de génération d'un pointeur qui ne pointe pas vers un élément du même tableau ou vers la fin) invoquent un comportement indéfini ...." de: en.cppreference.com / w / cpp / language / operator_arithmetic
Richard Critten
3
Eh bien, c'est similaire à un std::vectoravec 0 élément. begin()est déjà égal à end()donc vous ne pouvez pas incrémenter un itérateur qui pointe au début.
Phil1970
1
@PeterMortensen Je pense que votre modification a changé le sens de la dernière phrase ("Ce dont je suis sûr -> Je ne sais pas pourquoi"), pourriez-vous s'il vous plaît revérifier?
Fabio dit Réintégrer Monica
@PeterMortensen: Le dernier paragraphe que vous avez modifié est devenu un peu moins lisible.
Itachi Uchiwa

Réponses:

32

Les pointeurs vers des éléments de tableaux sont autorisés à pointer vers un élément valide ou après la fin. Si vous incrémentez un pointeur d'une manière qui va plus d'un après la fin, le comportement n'est pas défini.

Pour votre tableau de taille 0, il ppointe déjà au-delà de la fin, il n'est donc pas autorisé de l'incrémenter.

Voir C ++ 17 8.7 / 4 concernant l' +opérateur ( ++a les mêmes restrictions):

f l'expression Ppointe vers l'élément x[i]d'un objet tableau xavec n éléments, les expressions P + Jet J + P(où Ja la valeur j) pointent vers l'élément (éventuellement hypothétique) x[i+j]si 0≤i + j≤n; sinon, le comportement n'est pas défini.

interjay
la source
2
Donc, le seul cas x[i]est le même que x[i + j]lorsque les deux iet jont la valeur 0?
Rami Yen
8
@RamiYen x[i]est le même élément que x[i+j]si j==0.
interjay
1
Ugh, je déteste la "zone crépusculaire" de la sémantique C ++ ... +1 cependant.
einpoklum
4
@ einpoklum-reinstateMonica: Il n'y a pas vraiment de zone crépusculaire. C'est juste que C ++ est cohérent même pour le cas N = 0. Pour un tableau de N éléments, il existe N + 1 valeurs de pointeur valides car vous pouvez pointer derrière le tableau. Cela signifie que vous pouvez commencer au début du tableau et incrémenter le pointeur N fois pour arriver à la fin.
MSalters
1
@MaximEgorushkin Ma réponse concerne ce que la langue permet actuellement. La discussion à propos de ce que vous souhaitez autoriser à la place est hors sujet.
interjay
2

Je suppose que vous avez déjà la réponse; Si vous regardez un peu plus profondément: Vous avez dit que l'incrémentation d'un itérateur off-the-end est UB ainsi: Cette réponse est dans ce qu'est un itérateur?

L'itérateur est juste un objet qui a un pointeur et incrémentant cet itérateur qui incrémente vraiment le pointeur qu'il a. Ainsi, dans de nombreux aspects, un itérateur est traité en termes de pointeur.

int arr [] = {0,1,2,3,4,5,6,7,8,9};

int * p = arr; // p pointe vers le premier élément en arr

++ p; // p pointe vers arr [1]

Tout comme nous pouvons utiliser des itérateurs pour parcourir les éléments d'un vecteur, nous pouvons utiliser des pointeurs pour parcourir les éléments d'un tableau. Bien sûr, pour ce faire, nous devons obtenir des pointeurs vers le premier et un après le dernier élément. Comme nous venons de le voir, nous pouvons obtenir un pointeur sur le premier élément en utilisant le tableau lui-même ou en prenant l'adresse du premier élément. Nous pouvons obtenir un pointeur off-the-end en utilisant une autre propriété spéciale des tableaux. Nous pouvons prendre l'adresse de l'élément inexistant un après le dernier élément d'un tableau:

int * e = & arr [10]; // pointeur juste après le dernier élément en arr

Ici, nous avons utilisé l'opérateur d'indice pour indexer un élément inexistant; arr a dix éléments, donc le dernier élément de arr est à la position d'index 9. La seule chose que nous pouvons faire avec cet élément est de prendre son adresse, ce que nous faisons pour initialiser e. Comme un itérateur off-the-end (§ 3.4.1, p. 106), un pointeur off-the-end ne pointe pas vers un élément. Par conséquent, nous ne pouvons pas déréférencer ou incrémenter un pointeur de fin.

Il s'agit de l'édition C ++ primer 5 de Lipmann.

Il est donc UB ne le faites pas.

Raindrop7
la source
-4

Au sens strict, ce n'est pas un comportement indéfini, mais défini par l'implémentation. Ainsi, bien que déconseillé si vous prévoyez de prendre en charge des architectures non traditionnelles, vous pouvez probablement le faire.

La citation standard donnée par interjay est bonne, indiquant UB, mais ce n'est que le deuxième meilleur résultat à mon avis, car elle traite de l'arithmétique pointeur-pointeur (curieusement, l'un est explicitement UB, tandis que l'autre ne l'est pas). Il y a un paragraphe traitant directement de l'opération dans la question:

[expr.post.incr] / [expr.pre.incr]
L'opérande doit être [...] ou un pointeur vers un type d'objet complètement défini.

Oh, attendez un instant, un type d'objet complètement défini? C'est tout? Je veux dire, vraiment, taper ? Vous n'avez donc pas du tout besoin d'un objet?
Il faut pas mal de lecture pour trouver un indice que quelque chose là-dedans pourrait ne pas être aussi bien défini. Parce que jusqu'à présent, il se lit comme si vous étiez parfaitement autorisé à le faire, sans restrictions.

[basic.compound] 3fait une déclaration sur le type de pointeur que l'on peut avoir, et n'étant aucun des trois autres, le résultat de votre opération tomberait clairement sous 3.4: pointeur invalide .
Cependant, cela ne dit pas que vous n'êtes pas autorisé à avoir un pointeur non valide. Au contraire, il répertorie certaines conditions normales très courantes (par exemple, la fin de la durée de stockage) où les pointeurs deviennent régulièrement invalides. C'est donc apparemment une chose admissible. Et en effet:

[basic.stc] 4 L'
indirection via une valeur de pointeur non valide et le passage d'une valeur de pointeur non valide à une fonction de désallocation ont un comportement indéfini. Toute autre utilisation d'une valeur de pointeur non valide a un comportement défini par l'implémentation.

Nous faisons un "tout autre" là-bas, donc ce n'est pas un comportement indéfini, mais défini par l'implémentation, donc généralement autorisé (sauf si l'implémentation dit explicitement quelque chose de différent).

Malheureusement, ce n'est pas la fin de l'histoire. Bien que le résultat net ne change plus à partir de maintenant, il devient plus déroutant, plus vous recherchez "pointeur":

[basic.compound]
Une valeur valide d'un type de pointeur d'objet représente l' adresse d'un octet en mémoire ou un pointeur nul. Si un objet de type T se trouve à une adresse A, [...] on dit qu'il pointe vers cet objet, quelle que soit la façon dont la valeur a été obtenue .
[Remarque: Par exemple, l'adresse au-delà de la fin d'un tableau serait considérée comme pointant vers un objet non lié du type d'élément du tableau qui pourrait se trouver à cette adresse. [...]].

Lire comme: OK, peu importe! Tant qu'un pointeur pointe quelque part dans la mémoire , je vais bien?

[basic.stc.dynamic.safety] Une valeur de pointeur est un pointeur dérivé en toute sécurité [bla bla]

Lire comme: OK, dérivé en toute sécurité, peu importe. Cela n'explique pas ce que c'est, ni ne dit que j'en ai réellement besoin. Dérivé en toute sécurité. Apparemment, je peux toujours avoir des pointeurs dérivés sans sécurité. Je suppose que le déréférencement ne serait probablement pas une si bonne idée, mais il est parfaitement possible de les avoir. Cela ne dit pas le contraire.

Une implémentation peut avoir une sécurité de pointeur détendue, auquel cas la validité d'une valeur de pointeur ne dépend pas du fait qu'il s'agisse d'une valeur de pointeur dérivée en toute sécurité.

Oh, donc ça n'a peut-être pas d'importance, juste ce que je pensais. Mais attendez ... "peut-être pas"? Cela signifie que cela peut aussi bien . Comment puis-je savoir?

Une implémentation peut également avoir une sécurité de pointeur stricte, auquel cas une valeur de pointeur qui n'est pas une valeur de pointeur dérivée en toute sécurité est une valeur de pointeur non valide, sauf si l'objet complet référencé a une durée de stockage dynamique et a été précédemment déclaré accessible.

Attendez, il est donc même possible que je doive appeler declare_reachable()chaque pointeur? Comment puis-je savoir?

Maintenant, vous pouvez convertir en intptr_t, qui est bien défini, donnant une représentation entière d'un pointeur dérivé en toute sécurité. Pour lequel, bien sûr, étant un entier, il est parfaitement légitime et bien défini de l'incrémenter à votre guise.
Et oui, vous pouvez convertir le intptr_tdos en un pointeur, qui est également bien défini. Juste, n'étant pas la valeur d'origine, il n'est plus garanti que vous ayez un pointeur dérivé en toute sécurité (évidemment). Pourtant, dans l'ensemble, à la lettre de la norme, tout en étant défini par l'implémentation, c'est une chose 100% légitime à faire:

[expr.reinterpret.cast] 5
Une valeur de type intégral ou de type énumération peut être explicitement convertie en pointeur. Un pointeur converti en un entier de taille suffisante et [...] de retour à la même valeur [...] d'origine de type pointeur; les mappages entre pointeurs et entiers sont par ailleurs définis par l'implémentation.

La prise

Les pointeurs ne sont que des entiers ordinaires, vous seul les utilisez comme pointeurs. Oh si seulement c'était vrai!
Malheureusement, il existe des architectures où ce n'est pas vrai du tout, et générer simplement un pointeur invalide (ne pas le déréférencer, simplement l'avoir dans un registre de pointeur) provoquera un piège.

Voilà donc la base de la «mise en œuvre définie». Cela, et le fait que l'incrémentation d'un pointeur quand vous le souhaitez, comme vous le souhaitez, pourrait bien sûr provoquer un débordement, ce que la norme ne veut pas traiter. La fin de l'espace d'adressage de l'application peut ne pas coïncider avec l'emplacement du débordement, et vous ne savez même pas s'il existe un débordement pour les pointeurs sur une architecture particulière. Dans l'ensemble, c'est un gâchis cauchemardesque qui n'a aucun rapport avec les avantages possibles.

La gestion de la condition d'un objet passé de l'autre côté est simple: l'implémentation doit simplement s'assurer qu'aucun objet n'est jamais alloué afin que le dernier octet de l'espace d'adressage soit occupé. C'est donc bien défini car il est utile et trivial de garantir.

Damon
la source
1
Votre logique est défectueuse. "Donc tu n'as pas besoin d'un objet du tout?" interprète mal la norme en se concentrant sur une seule règle. Cette règle concerne le temps de compilation, que votre programme soit bien formé. Il y a une autre règle concernant le temps d'exécution. Ce n'est qu'au moment de l'exécution que vous pouvez réellement parler de l'existence d'objets à une certaine adresse. votre programme doit respecter toutes les règles; les règles au moment de la compilation au moment de la compilation et les règles au moment de l'exécution.
MSalters
5
Vous avez des défauts de logique similaires avec "OK, peu importe! Tant qu'un pointeur pointe quelque part dans la mémoire, je vais bien?". Non. Vous devez suivre toutes les règles. Le langage difficile à propos de "la fin d'un tableau étant le début d'un autre tableau" donne simplement à l' implémentation la permission d'allouer de la mémoire de manière contiguë; il n'a pas besoin de conserver de l'espace libre entre les allocations. Cela signifie que votre code peut avoir la même valeur A à la fois à la fin d'un objet tableau et au début d'un autre.
MSalters
1
"Un piège" n'est pas quelque chose qui peut être décrit par un comportement "défini par l'implémentation". Notez qu'interjay a trouvé la restriction sur l' +opérateur (d'où ++découle) ce qui signifie que le pointage après "un après la fin" n'est pas défini.
Martin Bonner soutient Monica
1
@PeterCordes: Veuillez lire le paragraphe 4 de basic.stc . Il indique "Comportement indéfini [...] d'indirection. Toute autre utilisation d'une valeur de pointeur non valide a un comportement défini par l'implémentation " . Je ne confond pas les gens en utilisant ce terme pour un autre sens. C'est le libellé exact. Ce n'est pas un comportement indéfini.
Damon
2
Il est à peine possible que vous ayez trouvé une faille pour le post-incrément, mais vous ne citez pas la section complète sur ce que fait le post-incrément. Je ne vais pas me pencher là-dessus pour le moment. Je suis d'accord que s'il y en a un, ce n'est pas prévu. Quoi qu'il en soit, aussi agréable que ce serait si ISO C ++ définissait plus de choses pour les modèles de mémoire plate, @MaximEgorushkin, il y a d'autres raisons (comme le bouclage du pointeur) pour ne pas autoriser les choses arbitraires. Voir les commentaires sur Les comparaisons de pointeurs doivent-elles être signées ou non signées en 64 bits x86?
Peter Cordes