¿Está definido el comportamiento de restar dos punteros NULL?

78

¿Está definida la diferencia de dos variables de puntero no nulas (según C99 y / o C ++ 98) si ambas están NULLvaloradas?

Por ejemplo, digamos que tengo una estructura de búfer que se parece a esto:

struct buf {
  char *buf;
  char *pwrite;
  char *pread;
} ex;

Digamos, ex.bufapunta a una matriz o alguna memoria mal ubicada. Si mi código siempre asegura eso pwritey preadapunta dentro de esa matriz o una más allá, entonces estoy bastante seguro de que ex.pwrite - ex.preadsiempre estará definido. Sin embargo, ¿qué pasa si pwritey preadambos son NULL? ¿Puedo esperar que restar los dos esté definido como (ptrdiff_t)0o el código estrictamente compatible necesita probar los punteros para NULL? Tenga en cuenta que el único caso que me interesa es cuando ambos punteros son NULL (lo que representa un caso de búfer no inicializado). La razón tiene que ver con una función "disponible" totalmente compatible, dado que se cumplen los supuestos anteriores:

size_t buf_avail(const struct s_buf *b)
{     
    return b->pwrite - b->pread;
}
John Luebs
fuente
1
¿Ha intentado realizar la operación más de una vez?
Hunter McMillen
10
¿Qué quieres decir? Sé a ciencia cierta que el resultado de esta operación es 0 en el 95% (digamos que el 5% es AS / 400) de las implementaciones y no pasará nada malo. No estoy interesado en los detalles de implementación. Mi pregunta se refiere a algunas definiciones estándar específicas.
John Luebs
8
Hunter McMillen: Ese es un mal enfoque: "Guardé el puntero en int y no pasó nada. Verifiqué en una computadora y compilador diferente y no pasó nada. Luego vinieron las computadoras de 64 bits". Si algo funciona ahora pero depende de un comportamiento indefinido, es posible que no funcione en el futuro.
Maciej Piechotka
3
Lo felicito por asegurarse de que su código esté garantizado para funcionar según los estándares relevantes en lugar de simplemente darse cuenta de que funcionó en las plataformas que probó.
David Schwartz
1
@TobySpeight: Un compilador 8086 tendrá una representación diferente para un nearpuntero nulo farcalificado de uno calificado, pero ¿usaría múltiples representaciones para farpunteros nulos ? Si un nearpuntero nulo se convierte en un farpuntero que a su vez se compara con un farpuntero nulo cuando, por ejemplo, DS es igual a 0x1234, qué sucede: (1) 0x0000 se convierte en 0x0000: 0x0000; (2) 0x0000 se convierte a 0x1234: 0x0000, pero el operador de comparación comprueba el caso de ambos segmentos-cero, o (3) 0x0000 se convierte a 0x1234: 0x0000, que compara desigual con 0x0000: 0x0000.
supercat

Respuestas:

100

En C99, es un comportamiento técnicamente indefinido. C99 §6.5.6 dice:

7) Para los propósitos de estos operadores, un puntero a un objeto que no es un elemento de una matriz se comporta de la misma manera que un puntero al primer elemento de una matriz de longitud uno con el tipo del objeto como su tipo de elemento.

[...]

9) Cuando se restan dos punteros, ambos apuntarán a elementos del mismo objeto de matriz, o uno más allá del último elemento del objeto de matriz; el resultado es la diferencia de los subíndices de los dos elementos de la matriz. [...]

Y §6.3.2.3 / 3 dice:

Una expresión de constante entera con el valor 0, o una expresión convertida a tipo void *, se denomina constante de puntero nulo. 55) Si una constante de puntero nulo se convierte en un tipo de puntero , se garantiza que el puntero resultante, llamado puntero nulo , se comparará de forma desigual con un puntero a cualquier objeto o función.

Entonces, dado que un puntero nulo no es igual a cualquier objeto, viola las condiciones previas de 6.5.6 / 9, por lo que es un comportamiento indefinido. Pero en la práctica, estaría dispuesto a apostar a que casi todos los compiladores devolverán un resultado de 0 sin efectos secundarios negativos.

En C89, también es un comportamiento indefinido, aunque la redacción del estándar es ligeramente diferente.

C ++ 03, por otro lado, tiene un comportamiento definido en esta instancia. El estándar hace una excepción especial para restar dos punteros nulos. C ++ 03 §5.7 / 7 dice:

Si el valor 0 se suma o se resta de un valor de puntero, el resultado se compara igual al valor de puntero original. Si dos punteros apuntan al mismo objeto o ambos apuntan uno más allá del final de la misma matriz o ambos son nulos, y los dos punteros se restan, el resultado se compara con el valor 0 convertido al tipo ptrdiff_t.

C ++ 11 (así como el último borrador de C ++ 14, n3690) tienen una redacción idéntica a C ++ 03, con solo el pequeño cambio de std::ptrdiff_ten lugar de ptrdiff_t.

Adam Rosenfield
fuente
14
Debido a que está completo, esta es la mejor respuesta actualmente.
John Dibling
Esto parece un descuido en el estándar que debe corregirse con "9) Cuando se restan dos punteros, si son iguales, el resultado es cero. De lo contrario, ambos apuntarán a elementos del mismo objeto de matriz ..."
R .. GitHub DEJA DE AYUDAR A ICE
@R .., para desambiguar debería decir "comparar igual" ¿no? Debido a que dos punteros nulos pueden no contener el mismo valor, "no son" iguales.
Jens Gustedt
También parece que el último borrador del próximo estándar C1X también tiene el mismo idioma. Espero que esto sea de hecho un descuido y que el comité de idiomas lo solucione.
Adam Rosenfield
Dos punteros nulos tienen el mismo valor en virtud de comparar iguales. Por supuesto, es posible que no tengan la misma representación .
R .. GitHub DEJA AYUDAR A ICE
36

Encontré esto en el estándar C ++ (5.7 [expr.add] / 7):

Si dos punteros [...] son ​​nulos y los dos punteros se restan, el resultado se compara con el valor 0 convertido al tipo std :: ptrdiff_t

Como han dicho otros, C99 requiere que la suma / resta entre 2 punteros sea del mismo objeto de matriz. NULL no apunta a un objeto válido, por lo que no puede usarlo en la resta.

Pubby
fuente
4
+1: Interesante, entonces C ++ define explícitamente este comportamiento, mientras que C no lo hace.
Oliver Charlesworth
23

Editar : esta respuesta solo es válida para C, no vi la etiqueta C ++ cuando respondí.

No, la aritmética de punteros solo está permitida para punteros que apuntan dentro del mismo objeto. Dado que, por definición de los punteros nulos estándar de C, no apuntan a ningún objeto, este es un comportamiento indefinido.

(Aunque, supongo que cualquier compilador razonable devolverá solo 0eso, pero quién sabe).

Jens Gustedt
fuente
5
Esto es incorrecto. Consulte 5.7 [expr.add] / 7: "Si dos punteros apuntan al mismo objeto o ambos señalan uno más allá del final de la misma matriz o ambos son nulos , y los dos punteros se restan, el resultado se compara con el valor 0 convertido al tipo std::ptrdiff_t".
CB Bailey
1
Esto es incorrecto en el contexto específico de C ++. ¿Qué pasa con los otros idiomas etiquetados?
John Dibling
8
Vaya, nunca pensé que esta tonta pregunta tocaría una diferencia de especificaciones de C / C ++.
John Luebs
3
@Jens: Las preguntas frecuentes y los administradores del sitio nos animan a editar las respuestas si podemos mejorarlas. Tu respuesta es mejor ahora que antes. No tenía la intención de ofender y, para ser honesta, dadas las políticas del sitio Creo que usted está fuera de línea para ser ofendido. Ver: stackoverflow.com/privileges/edit
John Dibling
2
Jens, normalmente estaría de acuerdo contigo. Intento nunca editar la respuesta de alguien, sino señalar mis desacuerdos con un comentario: menos plumas se alborotan y es posible que aprendan algo. O tal vez me equivoque y mi edición sería contraproducente. Pero en este caso, creo que la edición de John estaba justificada, porque su respuesta fue mejor calificada pero claramente no 100% correcta. Era necesario evitar que la gente "se amontonara" para dar una respuesta correcta sin considerar las alternativas.
Mark Ransom
0

El Estándar C no impone ningún requisito sobre el comportamiento en este caso, pero muchas implementaciones especifican el comportamiento de la aritmética de punteros en muchos casos más allá de los mínimos requeridos por el Estándar, incluido este.

En cualquier implementación de C conforme, y casi todas (si no todas) las implementaciones de dialectos similares a C, las siguientes garantías serán válidas para cualquier puntero ptal que identifique *po *(p-1)identifique algún objeto:

  • Para cualquier valor entero zque sea igual a cero, el puntero valora (p+z)y (p-z)será equivalente en todos los sentidos a p, excepto que solo serán constantes si ambos py zson constantes.
  • Para cualquiera qque sea equivalente a p, las expresiones p-qy q-pambos darán cero.

Tener tales garantías válidas para todos los valores de puntero, incluido el nulo, puede eliminar la necesidad de algunas comprobaciones nulas en el código de usuario. Además, en la mayoría de las plataformas, generar código que mantenga dichas garantías para todos los valores de puntero sin tener en cuenta si son nulos sería más sencillo y económico que tratar los nulos de forma especial. Algunas plataformas, sin embargo, pueden atrapar intentos de realizar aritmética de punteros con punteros nulos, incluso al sumar o restar cero. En tales plataformas, la cantidad de verificaciones nulas generadas por el compilador que tendrían que agregarse a las operaciones de puntero para mantener la garantía excedería en muchos casos la cantidad de verificaciones nulas generadas por el usuario que podrían omitirse como resultado.

Si hubiera una implementación donde el costo de mantener las garantías sería grande, pero pocos programas recibirían algún beneficio de ellos, tendría sentido permitirle atrapar cálculos "nulos + cero" y requerir ese código de usuario para tal implementación incluye los controles manuales nulos que las garantías podrían haber hecho innecesarias. No se esperaba que dicha provisión afectara al 99,44% restante de las implementaciones, donde el valor de mantener las garantías excedería el costo. Tales implementaciones deberían mantener tales garantías, pero sus autores no deberían necesitar que los autores del Estándar les digan eso.

Los autores de C ++ han decidido que las implementaciones conformes deben mantener las garantías anteriores a cualquier costo, incluso en plataformas donde podrían degradar sustancialmente el rendimiento de la aritmética de punteros. Juzgaron que el valor de las garantías, incluso en plataformas donde sería costoso mantenerlas, excedería el costo. Tal actitud puede haber sido afectada por el deseo de tratar C ++ como un lenguaje de nivel superior que C. Se podría esperar que el programador de CA sepa cuándo una plataforma de destino en particular manejaría casos como (nulo + cero) de manera inusual, pero los programadores de C ++ no se esperaba que se preocuparan por esas cosas. Por lo tanto, se consideró que valía la pena el costo de garantizar un modelo de comportamiento coherente.

Por supuesto, hoy en día las preguntas sobre lo que está "definido" rara vez tienen algo que ver con los comportamientos que puede soportar una plataforma. En cambio, ahora está de moda que los compiladores, en nombre de la "optimización", requieran que los programadores escriban código manualmente para manejar casos de esquina que las plataformas anteriormente habrían manejado correctamente. Por ejemplo, si el código que se supone que genera ncaracteres que comienzan en la dirección pse escribe como:

void out_characters(unsigned char *p, int n)
{
  unsigned char *end = p+n;
  while(p < end)
    out_byte(*p++);
}

Los compiladores más antiguos generarían código que no generaría de manera confiable nada, sin efectos secundarios, si p == NULL yn == 0, sin necesidad de un caso especial n == 0. Sin embargo, en compiladores más nuevos, habría que agregar código adicional:

void out_characters(unsigned char *p, int n)
{
  if (n)
  {
    unsigned char *end = p+n;
    while(p < end)
      out_byte(*p++);
  }
}

del cual un optimizador puede o no puede deshacerse. Si no se incluye el código adicional, algunos compiladores pueden pensar que, dado que p "no puede ser nulo", es posible que se omitan las comprobaciones posteriores de puntero nulo, lo que provocará que el código se rompa en un lugar no relacionado con el "problema" real.

Super gato
fuente