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] 3
fait 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_t
dos 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.