Acesso unidimensional a um array multidimensional: é um comportamento bem definido?

Eu imagino que todos nós concordamos que é considerado C idiomático para acessar um verdadeiro array multidimensional, desreferenciando um ponteiro (possivelmente offset) para o seu primeiro elemento de uma forma unidimensional, por exemplo:

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); 

No entanto, o advogado da língua em mim precisa convencer que isso é realmente bem definido C! Em particular:

  1. O padrão garante que o compilador não colocará preenchimento entre, por exemplo, mtx[0][2] e mtx[1][0] ?

  2. Normalmente, a indexação do final de um array (diferente de one-past the end) é indefinida (C99, 6.5.6 / 8). Então, o seguinte é 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]; 

    Portanto, pela mesma regra, espera-se que o seguinte seja indefinido:

     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? 

    Então, por que isso deveria ser definido?

     int mtx[5][3]; int *p = &(&mtx[0][0])[7]; 

Então, qual parte do padrão C permite explicitamente isso? (Vamos supor que o c99 seja uma questão de discussão.)

EDITAR

Note que não tenho dúvidas de que isso funciona bem em todos os compiladores. O que estou perguntando é se isso é explicitamente permitido pelo padrão.

O único obstáculo para o tipo de access que você deseja fazer é que os objects do tipo int [5][3] e int [15] não tenham permissão para aliases um ao outro. Portanto, se o compilador estiver ciente de que um ponteiro do tipo int * aponta para um dos int [3] arrays do primeiro, ele poderia impor restrições de limites de array que impediriam o access a qualquer coisa fora desse array int [3] .

Você pode contornar esse problema colocando tudo dentro de uma união que contenha tanto o array int [5][3] quanto o array int [15] , mas eu não sei se a união hacks que as pessoas usam para digitar -punning são realmente bem definidos. Este caso pode ser um pouco menos problemático, uma vez que você não seria um tipo de punção de células individuais, apenas a lógica da matriz, mas ainda não tenho certeza.

Um caso especial que deve ser observado: se o seu tipo fosse unsigned char (ou qualquer tipo de char ), acessar o array multidimensional como um array unidimensional seria perfeitamente bem definido. Isso ocorre porque a matriz unidimensional de unsigned char que se sobrepõe a ela é explicitamente definida pelo padrão como a “representação” do object e é inerentemente permitida a alias.

Todos os arrays (incluindo os multidimensionais) são livres de preenchimento. Mesmo que nunca seja explicitamente mencionado, pode ser inferido a partir do sizeof regras.

Agora, a assinatura de array é um caso especial de aritmética de ponteiro, e C99 seção 6.5.6, §8 afirma claramente que o comportamento é definido apenas se o operando ponteiro e o ponteiro resultante estiverem na mesma matriz (ou um elemento passado), o que torna implementações de verificação de limites da linguagem C possíveis.

Isso significa que seu exemplo é, na verdade, comportamento indefinido. No entanto, como a maioria das implementações de C não verifica os limites, ele funcionará como esperado – a maioria dos compiladores trata expressões de ponteiro indefinidas como

 mtx[0] + 5 

de forma idêntica a contrapartes bem definidas como

 (int *)((char *)mtx + 5 * sizeof (int)) 

que é bem definido porque qualquer object (incluindo toda a matriz bidimensional) pode sempre ser tratado como uma matriz unidimensional do tipo char .


Mais uma meditação sobre o texto da seção 6.5.6, dividindo o access fora dos limites a uma subexpressão aparentemente bem definida como

 (mtx[0] + 3) + 2 

raciocinando que mtx[0] + 3 é um ponteiro para um elemento após o final de mtx[0] (tornando a primeira adição bem definida) e também como um ponteiro para o primeiro elemento de mtx[1] (fazendo o segundo adição bem definida) está incorreta:

Mesmo que mtx[0] + 3 e mtx[1] + 0 tenham a garantia de serem iguais (ver seção 6.5.9, §6), eles são semanticamente diferentes. Por exemplo, o primeiro não pode ser desreferenciado e, portanto, não aponta para um elemento de mtx[1] .

  1. É certo que não há preenchimento entre os elementos de um array.

  2. Há provisão para fazer computação de endereço em tamanho menor que o espaço de endereço completo. Isso poderia ser usado, por exemplo, no modo enorme de 8086, para que a parte do segmento nem sempre fosse atualizada se o compilador soubesse que você não poderia cruzar um limite de segmento. (Há muito tempo atrás, para eu lembrar se os compiladores que usei se beneficiaram disso ou não).

Com o meu modelo interno – não tenho certeza se é exatamente o mesmo que o modelo padrão e é muito doloroso verificar, as informações sendo distribuídas em todos os lugares –

  • o que você está fazendo no clearBottomRightElement é válido.

  • int *p = &foo.row[7]; é indefinido

  • int i = mtx[0][5]; é indefinido

  • int *p = &row[7]; não compila (gcc concorda comigo)

  • int *p = &(&mtx[0][0])[7]; está na zona cinzenta (da última vez que verifiquei em detalhes algo como isto, acabei por considerar C90 inválido e C99 válido, poderia ser o caso aqui ou eu poderia ter perdido alguma coisa).

Minha compreensão do padrão C99 é que não exigência de que os arrays multidimensionais devem ser dispostos em uma ordem contígua na memory. Seguindo a única informação relevante que encontrei no padrão (cada dimensão é garantida para ser contígua).

Se você quiser usar o access x [COLS * r + c], sugiro que se atenha aos arrays de dimensão única.

Subscrição de matriz

Operadores subscritos sucessivos designam um elemento de um object de multidimensional array. Se E é uma matriz n-dimensional (n ≥ 2) com dimensões i × j ×. . . × k, então E (usado como diferente de lvalue) é convertido em um ponteiro para uma matriz (n – 1) com dimensões j ×. . . × k Se o operador unário * for aplicado a esse ponteiro explicitamente, ou implicitamente como resultado de subscrição, o resultado será o array apontado para (n-1) -dimensional, que por sua vez é convertido em um ponteiro se usado como diferente de lvalue . Segue-se que as matrizes são armazenadas em ordem de linha maior (o último índice varia mais rápido).

Tipo de matriz

– Um tipo de matriz descreve um conjunto não vazio e contido de objects com um tipo de object de membro específico, chamado de tipo de elemento. 36) Os tipos de matriz são caracterizados por seu tipo de elemento e pelo número de elementos na matriz. Diz-se que um tipo de array é derivado de seu tipo de elemento, e se seu tipo de elemento é T, o tipo de matriz é às vezes chamado de ” array de T ”. A construção de um tipo de matriz de um tipo de elemento é chamada de ” derivação de tipo de matriz ”.