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

34

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, pactúa como un elemento del pasado. Puedo imprimir la dirección que papunta.

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?
Itachi Uchiwa
fuente
44
UB "... Cualquier otra situación (es decir, intentos de generar un puntero que no apunte a un elemento de la misma matriz o uno más allá del final) invoca un comportamiento indefinido ..." from: en.cppreference.com / w / cpp / language / operator_arithmetic
Richard Critten
3
Bueno, esto es similar a un std::vectorelemento con 0. begin()ya es igual a, end()por lo que no puede incrementar un iterador que apunta al principio.
Phil1970
1
@PeterMortensen Creo que su edición cambió el significado de la última oración ("De lo que estoy seguro -> No estoy seguro de por qué"), ¿podría verificarlo?
Fabio dice reinstalar a Mónica el
@PeterMortensen: el último párrafo que ha editado se ha vuelto un poco menos legible.
Itachi Uchiwa

Respuestas:

32

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, pya está apuntando una más allá del final, por lo que no está permitido incrementarla.

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

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

interjay
fuente
2
Por lo que el único caso x[i]es el mismo que x[i + j]es cuando ambos iy jtener el valor 0?
Rami Yen
8
@RamiYen x[i]es el mismo elemento que x[i+j]si j==0.
Interjay
1
Ugh, odio la "zona crepuscular" de la semántica de C ++ ... +1 sin embargo.
einpoklum
44
@ einpoklum-reinstateMonica: Realmente no hay zona de penumbra. Es solo que C ++ es consistente incluso para el caso N = 0. Para una matriz de N elementos, hay N + 1 valores de puntero válidos porque puede apuntar detrás de la matriz. Eso significa que puede comenzar al principio de la matriz e incrementar el puntero N veces para llegar al final.
MSalters el
1
@MaximEgorushkin Mi respuesta es sobre lo que el idioma permite actualmente. La discusión sobre usted le gustaría que permitiera en su lugar está fuera de tema.
Interjay
2

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.

Gota de lluvia7
fuente
-4

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] 3hace 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_tposterior 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.

Damon
fuente
1
Tu lógica es defectuosa. "¿Entonces no necesitas un objeto en absoluto?" malinterpreta el Estándar al enfocarse en una sola regla. Esa regla es sobre el tiempo de compilación, si su programa está bien formado. Hay otra regla sobre el tiempo de ejecución. Solo en tiempo de ejecución puedes hablar sobre la existencia de objetos en una determinada dirección. su programa necesita cumplir con todas las reglas; las reglas de tiempo de compilación en tiempo de compilación y las reglas de tiempo de ejecución en tiempo de ejecución.
MSalters el
55
Tienes fallas lógicas similares con "OK, ¿a quién le importa? Mientras un puntero apunte a algún lugar de la memoria, ¿estoy bien?". No. Tienes que seguir todas las reglas. El lenguaje difícil sobre "el final de una matriz es el comienzo de otra matriz" simplemente le da permiso a la implementación para asignar memoria contiguamente; no necesita mantener espacio libre entre asignaciones. Eso significa que su código podría tener el mismo valor A tanto como el final de un objeto de matriz como el comienzo de otro.
MSalters el
1
"Una trampa" no es algo que pueda describirse mediante el comportamiento "definido por la implementación". Tenga en cuenta que interjay ha encontrado la restricción en el +operador (desde el que ++fluye), lo que significa que señalar después de "uno después del final" no está definido.
Martin Bonner apoya a Monica el
1
@ PeterCordes: Lea basic.stc, párrafo 4 . Dice "Comportamiento [...] indefinido de indirección. Cualquier otro uso de un valor de puntero no válido tiene un comportamiento definido por la implementación " . No estoy confundiendo a las personas al usar ese término para otro significado. Es la redacción exacta. Se no indefinido comportamiento.
Damon el
2
Es casi imposible que haya encontrado una laguna para el post-incremento, pero no cita la sección completa sobre lo que hace el post-incremento. No voy a investigar eso ahora mismo. Acordó que si hay uno, no es intencionado. De todos modos, tan bueno como sería si ISO C ++ definiera más cosas para los modelos de memoria plana, @MaximEgorushkin, hay otras razones (como el ajuste del puntero) para no permitir cosas arbitrarias. Ver comentarios sobre ¿Deberían las comparaciones de punteros estar firmadas o no en x86 de 64 bits?
Peter Cordes el