Acceso unidimensional a una matriz multidimensional: ¿es un comportamiento bien definido?
Me imagino que todos estamos de acuerdo en que se considera C idiomático acceder a una matriz multidimensional verdadera eliminando la referencia a un puntero (posiblemente desplazado) a su primer elemento de forma unidimensional, por ejemplo:
void clearBottomRightElement(int *array, int M, int N)
{
array[M*N-1] = 0; // Pretend the array is one-dimensional
}
int mtx[5][3];
...
clearBottomRightElement(&mtx[0][0], 5, 3);
Sin embargo, el abogado del lenguaje que hay en mí necesita convencerse de que en realidad se trata de un C bien definido. En particular:
¿El estándar garantiza que el compilador no colocará relleno entre, por ejemplo,
mtx[0][2]
ymtx[1][0]
?Normalmente, la indexación del final de una matriz (que no sea una vez después del final) no está definida (C99, 6.5.6/8). Entonces lo siguiente está claramente indefinido:
struct { int row[3]; // The object in question is an int[3] int other[10]; } foo; int *p = &foo.row[7]; // ERROR: A crude attempt to get &foo.other[4];
Entonces, según la misma regla, uno esperaría que lo siguiente no estuviera definido:
int mtx[5][3]; int (*row)[3] = &mtx[0]; // The object in question is still an int[3] int *p = &(*row)[7]; // Why is this any better?
Entonces, ¿por qué debería definirse esto?
int mtx[5][3]; int *p = &(&mtx[0][0])[7];
Entonces, ¿qué parte del estándar C permite esto explícitamente? (Asumamosc99por el bien de la discusión.)
EDITAR
Tenga en cuenta que no tengo dudas de que esto funciona bien en todos los compiladores. Lo que pregunto es si esto está explícitamente permitido por el estándar.
Todas las matrices (incluidas las multidimensionales) no tienen relleno. Incluso si nunca se menciona explícitamente, se puede inferir de sizeof
las reglas.
Ahora, la suscripción a una matriz es un caso especial de aritmética de punteros, y la sección 6.5.6, §8 del C99 establece claramente que el comportamiento sólo se define si el operando del puntero y el puntero resultante se encuentran en la misma matriz (o en un elemento anterior), lo que hace que Posibilidad de implementar implementaciones de verificación de límites del lenguaje C.
Esto significa que su ejemplo es, de hecho, un comportamiento indefinido. Sin embargo, como la mayoría de las implementaciones de C no verifican los límites, funcionará como se esperaba; la mayoría de los compiladores tratan las expresiones de puntero indefinidas como
mtx[0] + 5
idénticamente a sus homólogos bien definidos como
(int *)((char *)mtx + 5 * sizeof (int))
que está bien definido porque cualquier objeto (incluida la matriz bidimensional completa) siempre puede tratarse como una matriz unidimensional de tipo char
.
Si reflexionamos más sobre la redacción de la sección 6.5.6, dividir el acceso fuera de límites en subexpresiones aparentemente bien definidas como
(mtx[0] + 3) + 2
El razonamiento que mtx[0] + 3
es un puntero a un elemento más allá del final de mtx[0]
(haciendo que la primera adición esté bien definida) y también un puntero al primer elemento de mtx[1]
(haciendo que la segunda adición esté bien definida) es incorrecto:
Aunque se garantiza que mtx[0] + 3
y mtx[1] + 0
son iguales (ver sección 6.5.9, §6), son semánticamente diferentes. Por ejemplo, no se puede eliminar la referencia al primero y, por lo tanto, no apunta a un elemento de mtx[1]
.
El único obstáculo para el tipo de acceso que desea realizar es que los objetos de tipo int [5][3]
y int [15]
no pueden tener alias entre sí. Por lo tanto, si el compilador sabe que un puntero de tipo int *
apunta a una de las int [3]
matrices del primero, podría imponer restricciones en los límites de la matriz que impedirían acceder a cualquier cosa fuera de esa int [3]
matriz.
Es posible que pueda solucionar este problema colocando todo dentro de una unión que contenga tanto la int [5][3]
matriz como la int [15]
matriz, pero no tengo muy claro si los trucos de unión que la gente usa para los juegos de palabras están realmente bien definidos. Este caso podría ser un poco menos problemático ya que no estaría escribiendo juegos de palabras con celdas individuales, solo con la lógica de la matriz, pero todavía no estoy seguro.
Un caso especial que debe tenerse en cuenta: si su tipo fuera unsigned char
(o cualquier char
tipo), acceder a la matriz multidimensional como una matriz unidimensional estaría perfectamente bien definido. Esto se debe a que el estándar define explícitamente la matriz unidimensional unsigned char
que se superpone como la "representación" del objeto, y está inherentemente permitido asignarle un alias.
Es seguro que no existe relleno entre los elementos de una matriz.
Existen disposiciones para realizar cálculos de direcciones en un tamaño más pequeño que el espacio de direcciones completo. Esto podría usarse, por ejemplo, en el modo enorme de 8086 para que la parte del segmento no siempre se actualice si el compilador supiera que no se puede cruzar el límite de un segmento. (Hace demasiado tiempo para recordar si los compiladores que utilicé se beneficiaron de eso o no).
Con mi modelo interno, no estoy seguro de que sea exactamente igual al modelo estándar y es demasiado doloroso comprobarlo, ya que la información se distribuye por todas partes.
lo que estás haciendo
clearBottomRightElement
es válido.int *p = &foo.row[7];
es indefinidoint i = mtx[0][5];
es indefinidoint *p = &row[7];
no compila (gcc está de acuerdo conmigo)int *p = &(&mtx[0][0])[7];
está en la zona gris (la última vez que verifiqué detalles como este, terminé considerando C90 no válido y C99 válido, podría ser el caso aquí o podría haberme perdido algo).