c++ pointers undefined-behavior dynamic-arrays

c++ - ¿Incrementar un puntero a una matriz dinámica de tamaño 0 no está definido?



pointers undefined-behavior (3)

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 el tipo de puntero que uno puede tener, y al no ser ninguno de los otros tres, el resultado de su operación caería claramente en 3.4: puntero invá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 es Comportamiento indefinido, sino definido por la implementación, por lo tanto generalmente permitido (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 con seguridad, 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 manera 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 a declare_reachable() en 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 intptr_t nuevo a 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 se puede convertir 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 (no 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 siempre que 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.

AFAIK, aunque no podemos crear una matriz de memoria estática de tamaño 0, pero podemos hacerlo con las dinámicas:

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

Como he leído, p actúa como un elemento de un extremo pasado. Puedo imprimir la dirección a la que apunta p .

if(p) cout << p << endl;

  • Aunque estoy seguro de que no podemos desreferenciar ese puntero (pasado-último elemento) como no podemos hacerlo con los iteradores (pasado-último elemento), pero de lo que no estoy seguro es si se incrementa ese puntero p ? ¿Es un comportamiento indefinido (UB) como con los iteradores?

    p++; // UB?


Los punteros a elementos de matrices pueden apuntar a un elemento válido, o uno más allá del final. Si incrementa un puntero de una manera que va más de un final, el comportamiento es indefinido.

Para su matriz de tamaño 0, p ya está apuntando uno más allá del final, por lo que no está permitido incrementarlo.

Ver C ++ 17 8.7 / 4 con respecto al operador + ( ++ tiene las mismas restricciones):

f la expresión P apunta al elemento x[i] de un objeto de matriz x con n elementos, las expresiones P + J y J + P (donde J tiene el valor j ) apuntan al elemento (posiblemente hipotético) x[i+j] si 0≤i + j≤n; de lo contrario, el comportamiento es indefinido.


Supongo que ya tienes la respuesta; Si miras un poco más profundo: has dicho que incrementar un iterador externo es UB, por lo tanto: ¿Esta respuesta está en qué es un iterador?

El iterador es solo un objeto que tiene un puntero y el incremento de ese iterador realmente incrementa el puntero que tiene. Así, en muchos aspectos, un iterador se maneja en términos de un puntero.

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

int * p = arr; // p apunta al primer elemento en arr

++ p; // p apunta a arr [1]

Así como podemos usar iteradores para atravesar los elementos en un vector, podemos usar punteros para atravesar los elementos en una matriz. Por supuesto, para hacerlo, necesitamos obtener punteros al primero y uno pasado el último elemento. Como acabamos de ver, podemos obtener un puntero al primer elemento utilizando la matriz en sí o tomando la dirección del primer elemento. Podemos obtener un puntero externo utilizando otra propiedad especial de las matrices. Podemos tomar la dirección del elemento inexistente más allá del último elemento de una matriz:

int * e = & arr [10]; // puntero justo después del último elemento en arr

Aquí usamos el operador de subíndice para indexar un elemento no existente; arr tiene diez elementos, por lo que el último elemento en arr está en la posición de índice 9. Lo único que podemos hacer con este elemento es tomar su dirección, que hacemos para inicializar e. Al igual que un iterador externo (§ 3.4.1, p. 106), un puntero externo no apunta a un elemento. Como resultado, no podemos desreferenciar o incrementar un puntero fuera del extremo.

Esto es de C ++ primer 5 edition de Lipmann.

Entonces es UB, no lo hagas.