¿C99 garantiza que las matrices son contiguas?

Después de un comentario candente en otra pregunta, comencé a debatir qué es y qué no está definido en el estándar C99 sobre las matrices C.

Básicamente cuando defino una matriz 2D como int a[5][5] , ¿la norma C99 garantiza o no que será un bloque contiguo de entradas, puedo convertirlo a (int *)a y estar seguro de que tendré una matriz 1D válida de 25 ints.

Como entiendo el estándar, la propiedad anterior está implícita en el tamaño de la definición y en la aritmética del puntero, pero otros parecen estar en desacuerdo y dicen que el moldear (int *) la estructura anterior da un comportamiento indefinido (incluso si aceptan que todas las implementaciones actuales valores contiguos).

Más específicamente, si pensamos en una implementación que instrumentaría las matrices para verificar los límites de las matrices para todas las dimensiones y devolver algún tipo de error al acceder a la matriz 1D, o no da acceso correcto a los elementos por encima de la 1ª fila. ¿Podría tal implementación ser un comstackdor estándar? Y en este caso, qué partes del estándar C99 son relevantes.

Deberíamos comenzar por inspeccionar qué es realmente un [5] [5]. Los tipos involucrados son:

  • En t
  • matriz [5] de enteros
  • matriz [5] de matrices

No hay una matriz [25] de enter involucrados.

Es correcto que el tamaño de la semántica implica que la matriz como un todo es contigua. La matriz [5] de ints debe tener 5 * sizeof (int) y aplicar recursivamente, a [5] [5] debe tener 5 * 5 * sizeof (int). No hay espacio para relleno adicional.

Además, la matriz como un todo debe estar funcionando cuando se le da a memset, memmove o memcpy con el tamaño de. También debe ser posible iterar sobre toda la matriz con a (char *). Entonces una iteración válida es:

 int a[5][5], i, *pi; char *pc; pc = (char *)(&a[0][0]); for (i = 0; i < 25; i++) { pi = (int *)pc; DoSomething(pi); pc += sizeof(int); } 

Hacer lo mismo con un (int *) sería un comportamiento indefinido, porque, como se dijo, no hay una matriz [25] de int involucrada. Usar una unión como en la respuesta de Christoph también debería ser válido. Pero hay otro punto que complica esto aún más, el operador de igualdad:

6.5.9.6 Dos punteros se comparan iguales si y solo si ambos son punteros nulos, ambos son punteros al mismo objeto (incluyendo un puntero a un objeto y un subobjeto al principio) o función, ambos son punteros a uno pasado al último elemento de el mismo objeto de matriz, o uno es un puntero a uno pasado el final de un objeto de matriz y el otro es un puntero al comienzo de un objeto de matriz diferente que pasa inmediatamente después del primer objeto de matriz en el espacio de direcciones. 91)

91) Dos objetos pueden estar adyacentes en la memoria porque son elementos adyacentes de una matriz más grande o miembros adyacentes de una estructura sin relleno entre ellos, o porque la implementación optó por colocarlos de esa manera, aunque no estén relacionados. Si las operaciones previas del puntero no válido (como los accesos fuera de los límites de la matriz) producen un comportamiento indefinido, las comparaciones posteriores también producen un comportamiento indefinido.

Esto significa para esto:

 int a[5][5], *i1, *i2; i1 = &a[0][0] + 5; i2 = &a[1][0]; 

i1 se compara como igual a i2. Pero al iterar sobre la matriz con un (int *), sigue siendo un comportamiento indefinido, ya que se deriva originalmente de la primera subcadena. No se convierte mágicamente a un puntero en el segundo subconjunto.

Incluso cuando haces esto

 char *c = (char *)(&a[0][0]) + 5*sizeof(int); int *i3 = (int *)c; 

no ayuda Se compara igual a i1 e i2, pero no se deriva de ninguno de los subarrays; es un puntero a una sola int o una matriz [1] de int en el mejor de los casos.

No considero esto un error en el estándar. Es al revés: Permitir esto introduciría un caso especial que viola el sistema de tipos para matrices o las reglas para la aritmética de punteros o ambas. Se puede considerar una definición faltante, pero no un error.

Entonces, incluso si el diseño de la memoria para un [5] [5] es idéntico al diseño de un [25], y el mismo bucle que usa un (char *) puede usarse para iterar sobre ambos, se permite que una implementación explote arriba si uno se usa como el otro. No sé por qué debería o saber alguna implementación que lo haría, y tal vez hay un solo hecho en el estándar que no se menciona hasta ahora que lo hace un comportamiento bien definido. Hasta entonces, lo consideraría indefinido y me mantendría en el lado seguro.

He agregado algunos comentarios más a nuestra discusión original .

sizeof semántica implica que int a[5][5] es contigua, pero visitar los 25 enteros mediante el incremento de un puntero como int *p = *a es un comportamiento indefinido: el puntero aritmético solo se define mientras todos los punteros estén dentro (o un elemento más allá del último elemento de) la misma matriz, como por ejemplo &a[2][1] y &a[3][1] no (ver C99 sección 6.5.6).

En principio, puede solucionar esto lanzando &a – que tiene tipo int (*)[5][5] – a int (*)[25] . Esto es legal de acuerdo con 6.3.2.3 §7, ya que no infringe ningún requisito de alineación. El problema es que el acceso a los enteros a través de este nuevo puntero es ilegal ya que viola las reglas de alias en 6.5 §7. Puede solucionar esto utilizando una union para el tipo de juego de palabras (vea la nota al pie 82 en TC3):

 int *p = ((union { int multi[5][5]; int flat[25]; } *)&a)->flat; 

Esto es, hasta donde puedo decir, estándares que cumplen con C99.

Si la matriz es estática, como su matriz int a[5][5] , se garantiza que será contigua.