En el sentido más estricto, este no es un Comportamiento indefinido, sino una implementación definida. Entonces, aunque no es aconsejable si planea admitir arquitecturas no convencionales, probablemente pueda hacerlo.
La cita estándar dada por interjay es buena, indicando UB, pero es solo el segundo mejor éxito en mi opinión, ya que trata con la aritmética puntero-puntero (curiosamente, uno es explícitamente UB, mientras que el otro no). Hay un párrafo que trata directamente sobre la operación en la pregunta:
[expr.post.incr] / [expr.pre.incr]
El operando será [...] o un puntero a un tipo de objeto completamente definido.
Oh, espera un momento, ¿un tipo de objeto completamente definido? ¿Eso es todo? Quiero decir, en serio, escriba ? ¿Entonces no necesitas un objeto en absoluto?
Se necesita bastante lectura para encontrar realmente una pista de que algo allí podría no estar tan bien definido. Porque hasta ahora, se lee como si estuviera perfectamente permitido hacerlo, sin restricciones.
[basic.compound] 3
hace una declaración sobre qué tipo de puntero puede tener uno, y al no ser ninguno de los otros tres, el resultado de su operación claramente se ubicaría en 3.4: puntero no válido .
Sin embargo, no dice que no se le permite tener un puntero no válido. Por el contrario, enumera algunas condiciones normales muy comunes (por ejemplo, la duración del final del almacenamiento) en las que los punteros se vuelven regularmente inválidos. Entonces, eso aparentemente es algo permitido. Y de hecho:
[basic.stc] 4
La indirección a través de un valor de puntero no válido y el paso de un valor de puntero no válido a una función de desasignación tienen un comportamiento indefinido. Cualquier otro uso de un valor de puntero no válido tiene un comportamiento definido por la implementación.
Estamos haciendo un "cualquier otro" allí, por lo que no se trata de Comportamiento indefinido, sino de implementación definida, por lo tanto generalmente permitida (a menos que la implementación explícitamente diga algo diferente).
Desafortunadamente, ese no es el final de la historia. Aunque el resultado neto ya no cambia a partir de ahora, se vuelve más confuso, cuanto más tiempo busque "puntero":
[basic.compound]
Un valor válido de un tipo de puntero de objeto representa la dirección de un byte en memoria o un puntero nulo. Si un objeto de tipo T se encuentra en una dirección [...] A se dice que apunta a ese objeto, independientemente de cómo se obtuvo el valor .
[Nota: Por ejemplo, se consideraría que la dirección que está más allá del final de una matriz apunta a un objeto no relacionado del tipo de elemento de la matriz que podría estar ubicado en esa dirección. [...]]
Lea como: OK, a quién le importa! Mientras un puntero apunte a algún lugar de la memoria , ¿estoy bien?
[basic.stc.dynamic.safety] Un valor de puntero es un puntero derivado de forma segura [bla, bla]
Lea como: OK, derivado de forma segura, lo que sea. No explica qué es esto, ni dice que realmente lo necesito. Seguramente derivado del diablo. Aparentemente, todavía puedo tener punteros no derivados de forma segura. Supongo que desreferenciarlos probablemente no sería una buena idea, pero es perfectamente permisible tenerlos. No dice lo contrario.
Una implementación puede tener una seguridad de puntero relajada, en cuyo caso la validez de un valor de puntero no depende de si es un valor de puntero derivado de forma segura.
Oh, entonces puede que no importe, solo lo que pensé. Pero espera ... "puede que no"? Eso significa que también puede hacerlo . ¿Cómo puedo saber?
Alternativamente, una implementación puede tener una estricta seguridad de puntero, en cuyo caso un valor de puntero que no es un valor de puntero derivado de forma segura es un valor de puntero no válido a menos que el objeto completo al que se hace referencia tenga una duración de almacenamiento dinámico y se haya declarado previamente accesible
Espera, ¿entonces es posible que necesite llamar declare_reachable()
a cada puntero? ¿Cómo puedo saber?
Ahora, puede convertir a intptr_t
, que está bien definido, dando una representación entera de un puntero derivado de forma segura. Para lo cual, por supuesto, al ser un número entero, es perfectamente legítimo y está bien definido incrementarlo a su antojo.
Y sí, puede convertir la parte intptr_t
posterior en un puntero, que también está bien definido. Solo que, al no ser el valor original, ya no se garantiza que tenga un puntero derivado de forma segura (obviamente). Aún así, en general, al pie de la letra, mientras se define la implementación, esto es algo 100% legítimo:
[expr.reinterpret.cast] 5
Un valor de tipo integral o tipo de enumeración puede convertirse explícitamente en un puntero. Un puntero convertido a un entero de tamaño suficiente y [...] de nuevo al mismo valor original [...] del tipo de puntero; las asignaciones entre punteros y enteros se definen de otra manera en la implementación.
La captura
Los punteros son enteros ordinarios, solo que los usas como punteros. ¡Oh, si eso fuera cierto!
Desafortunadamente, existen arquitecturas donde eso no es cierto en absoluto, y simplemente generar un puntero no válido (sin desreferenciarlo, solo tenerlo en un registro de puntero) causará una trampa.
Esa es la base de la "implementación definida". Eso, y el hecho de que incrementar un puntero cuando lo desee, por supuesto , podría causar un desbordamiento, que el estándar no quiere tratar. El final del espacio de direcciones de la aplicación puede no coincidir con la ubicación del desbordamiento, y usted ni siquiera sabe si existe un desbordamiento para los punteros en una arquitectura particular. En general, es un desastre de pesadilla, no en relación con los posibles beneficios.
Tratar con la condición de un objeto pasado por el otro lado, es fácil: la implementación simplemente debe asegurarse de que nunca se asigne ningún objeto para que el último byte en el espacio de direcciones esté ocupado. Así que está bien definido, ya que es útil y trivial para garantizar.