una ¿Qué significa realmente imposibilidad de devolver matrices en C?




punteros y arreglos en c (4)

Primero que nada, sí, puede encapsular una matriz en una estructura, y luego hacer lo que quiera con esa estructura (asignarla, devolverla desde una función, etc.).

En segundo lugar, como ha descubierto, el compilador tiene poca dificultad para emitir código para devolver (o asignar) estructuras. Así que esa no es la razón por la que tampoco puedes devolver matrices.

La razón fundamental por la que no puede hacer esto es que, de manera clara, las matrices son estructuras de datos de segunda clase en C. Todas las demás estructuras de datos son de primera clase. ¿Cuáles son las definiciones de "primera clase" y "segunda clase" en este sentido? Simplemente que los tipos de segunda clase no pueden ser asignados.

(Es probable que su próxima pregunta sea "Aparte de las matrices, ¿existen otros tipos de datos de segunda clase?", Y creo que la respuesta es "No realmente, a menos que cuente las funciones").

El hecho de que no puede devolver (o asignar) arreglos es que tampoco hay valores de tipo de arreglo. Hay objetos (variables) de tipo de matriz, pero cuando intentas tomar el valor de uno, obtienes un puntero al primer elemento de la matriz. [Nota al pie: más formalmente, no hay valores de tipo de matriz, aunque un objeto de tipo de matriz se puede considerar como un lvalor , aunque no asignable.]

Entonces, aparte del hecho de que no se puede asignar a una matriz, tampoco se puede generar un valor para asignar a una matriz. Si usted dice

char a[10], b[10];
a = b;

es como si hubieras escrito

a = &b[0];

Así que tenemos un puntero a la derecha y una matriz a la izquierda, y tendríamos una falta de coincidencia de tipo masiva, incluso si las matrices de alguna manera fueran asignables. Similarmente (de tu ejemplo) si intentamos escribir

a = f();

y en algún lugar dentro de la definición de la función f() tenemos

char ret[10];
/* ... fill ... */
return ret;

es como si la última línea dijera

return &ret[0];

y, nuevamente, no tenemos ningún valor de matriz para devolver y asignar a, simplemente un puntero.

(En el ejemplo de llamada de función, también tenemos el problema muy importante de que ret es una matriz local, peligrosa para tratar de regresar en C. Más sobre este punto más adelante).

Ahora, parte de tu pregunta es probablemente "¿Por qué es así?", Y también "Si no puedes asignar matrices, ¿por qué puedes asignar estructuras que contienen matrices?"

Lo que sigue es mi interpretación y mi opinión, pero es consistente con lo que Dennis Ritchie describe en el documento El desarrollo del lenguaje C.

La no asignabilidad de los arreglos surge de tres hechos:

  1. La intención de C es ser sintáctica y semánticamente cerca del hardware de la máquina. Una operación elemental en C debería compilar hasta una o unas pocas instrucciones de la máquina tomando uno o unos cuantos ciclos de procesador.

  2. Las matrices siempre han sido especiales, especialmente en la forma en que se relacionan con los punteros; esta relación especial evolucionó y fue fuertemente influenciada por el tratamiento de los arreglos en el lenguaje B predecesor de C

  3. Las estructuras no estaban inicialmente en C.

Debido al punto 2, es imposible asignar arrays, y debido al punto 1, no debería ser posible de todos modos, porque un solo operador de asignación = no debería expandirse al código que podría tomar N mil ciclos para copiar una matriz de N mil elementos .

Y luego llegamos al punto 3, que realmente termina formando una contradicción.

Cuando C obtuvo estructuras, al principio tampoco eran de primera clase, ya que no se podían asignar ni devolver. Pero la razón por la que no podías era simplemente que el primer compilador no era lo suficientemente inteligente, al principio, para generar el código. No hubo obstáculos sintácticos o semánticos, como los arreglos.

Y el objetivo en todo momento era que las estructuras fueran de primera clase, y esto se logró relativamente pronto, poco después de la fecha en que se iba a imprimir la primera edición de K&R.

Pero la gran pregunta sigue siendo, si se supone que una operación elemental se compile en un pequeño número de instrucciones y ciclos, ¿por qué ese argumento no permite la asignación de estructura? Y la respuesta es, sí, es una contradicción.

Creo (aunque esto es más especulación de mi parte) que el pensamiento era algo así: "Los tipos de primera clase son buenos, los de segunda clase son desafortunados. Estamos atascados con el estado de segunda clase para arreglos, pero podemos hacerlo mejor con las estructuras. La regla del código sin costo no es realmente una regla, es más bien una pauta. Las matrices a menudo serán grandes, pero las estructuras generalmente serán pequeñas, decenas o cientos de bytes, por lo que asignarlas no lo hará. Por lo general, ser demasiado caro ".

Por lo tanto, una aplicación coherente de la regla del código sin costo cayó en el camino. C nunca ha sido perfectamente regular o consistente, de todos modos. (En realidad, tampoco son la gran mayoría de los idiomas exitosos, tanto humanos como artificiales).

Con todo esto dicho, puede valer la pena preguntar: "¿Qué pasaría si C apoyara la asignación y devolución de matrices? ¿Cómo podría funcionar?" Y la respuesta tendrá que implicar alguna forma de desactivar el comportamiento predeterminado de las matrices en las expresiones, es decir, que tienden a convertirse en punteros a su primer elemento.

En algún momento, en los años 90, IIRC, hubo una propuesta bastante bien pensada para hacer exactamente esto. Creo que incluía encerrar una expresión de matriz en [ ] o [[ ]] o algo así. Hoy parece que no puedo encontrar ninguna mención de esa propuesta (aunque le agradecería que alguien me proporcionara una referencia). Por ahora, voy a plantear la hipótesis de un nuevo operador o pseudofunción llamado arrayval() .

Podríamos extender C para permitir la asignación de matrices haciendo lo siguiente:

  1. Elimine la prohibición de usar una matriz en el lado izquierdo de un operador de asignación.

  2. Eliminar la prohibición de declarar funciones con valores de matriz. Volviendo a la pregunta original, haga que char f(void)[8] { ... } legal.

  3. (Este es el problema). Tenga una forma de mencionar una matriz en una expresión y termine con un valor verdadero, asignable (un valor nominal ) de tipo matriz. Como se mencionó, por ahora voy a postular la sintaxis arrayval( ... ) .

[Nota: Hoy tenemos una " definición clave " que

Una referencia a un objeto de tipo de matriz que aparece en una expresión se descompone (con tres excepciones) en un puntero a su primer elemento.

Las tres excepciones son cuando la matriz es el operando de un operador sizeof o & , o es un inicializador literal de cadena para una matriz de caracteres. Bajo las modificaciones hipotéticas que estoy discutiendo aquí, habría cuatro excepciones, con el operando de un operador de arrayval agregado a la lista.]

De todos modos, con estas modificaciones en su lugar, podríamos escribir cosas como

char a[8], b[8] = "Hello";
a = arrayval(b);

(Obviamente, también tendríamos que decidir qué hacer si a y b no fueran del mismo tamaño).

Dada la función prototipo.

char f(void)[8];

también podríamos hacer

a = f();

Veamos la definición hipotética de f . Podríamos tener algo como

char f(void)[8] {
    char ret[8];
    /* ... fill ... */
    return arrayval(ret);
}

Tenga en cuenta que (con la excepción del hipotético nuevo operador arrayval() ) esto es solo sobre lo que originalmente publicó Darío Rodríguez. También tenga en cuenta que, en el hipotético mundo donde la asignación de matrices era legal, y que arrayval() algo como arrayval() , ¡esto funcionaría! En particular, no sufriría el problema de devolver un puntero que pronto será inválido a la matriz local ret . Devolvería una copia de la matriz, por lo que no habría ningún problema en absoluto, sería casi perfectamente análogo a lo obviamente legal.

int g(void) {
    int ret;
    /* ... compute ... */
    return ret;
}

Finalmente, volviendo a la pregunta secundaria de "¿Hay otros tipos de segunda clase?", Creo que es más que una coincidencia que las funciones, como las matrices, tomen su dirección automáticamente cuando no se usan como sí mismas (es decir, como funciones o matrices), y que de manera similar no hay valores de tipo de función. Pero esto es sobre todo una reflexión ociosa, porque no creo que haya escuchado funciones referidas como tipos de "segunda clase" en C. (Quizás lo hayan hecho, y lo he olvidado).

No estoy intentando replicar la pregunta habitual acerca de que C no puede devolver matrices, sino profundizar un poco más en ella.

No podemos hacer esto:

char f(void)[8] {
    char ret;
    // ...fill...
    return ret;
}

int main(int argc, char ** argv) {
    char obj_a[10];
    obj_a = f();
}

Pero podemos hacer:

struct s { char arr[10]; };

struct s f(void) {
    struct s ret;
    // ...fill...
    return ret;
}

int main(int argc, char ** argv) {
    struct s obj_a;
    obj_a = f();
}

Por lo tanto, estaba hojeando el código ASM generado por gcc -S y parece estar trabajando con la pila, dirigiéndome a -x(%rbp) como con cualquier otro retorno de la función C.

¿Qué es con la devolución de matrices directamente? Quiero decir, no en términos de optimización o complejidad computacional, sino en términos de la capacidad real de hacerlo sin la capa de estructura.

Datos adicionales: estoy usando Linux y gcc en un x64 Intel.


Para que los arreglos sean objetos de primera clase, usted esperaría al menos poder asignarlos. Pero eso requiere conocimiento del tamaño, y el sistema de tipo C no es lo suficientemente poderoso como para adjuntar tamaños a ningún tipo. C ++ podría hacerlo, pero no debido a problemas heredados: tiene referencias a arreglos de tamaño particular ( typedef char (&some_chars)[32] ), pero los arreglos simples aún se convierten implícitamente en punteros como en C. C ++ ha std: : array en su lugar, que es básicamente el array-within-struct mencionado más un poco de azúcar sintáctico.


¿Qué es con la devolución de matrices directamente? Quiero decir, no en términos de optimización o complejidad computacional, sino en términos de la capacidad real de hacerlo sin la capa de estructura.

No tiene nada que ver con la capacidad per se . Otros idiomas ofrecen la capacidad de devolver matrices, y ya sabe que en C puede devolver una estructura con un miembro de matriz. Por otro lado, otros idiomas tienen la misma limitación que C, y más aún. Java, por ejemplo, no puede devolver matrices, ni objetos de ningún tipo, desde métodos. Solo puede devolver primitivas y referencias a objetos.

No, es simplemente una cuestión de diseño de lenguaje. Como con la mayoría de las otras cosas que tienen que ver con las matrices, los puntos de diseño aquí giran en torno a la disposición de C de que las expresiones de tipo de matriz se convierten automáticamente a punteros en casi todos los contextos. El valor proporcionado en una declaración de return no es una excepción, por lo que C no tiene forma de expresar siquiera la devolución de una matriz en sí. Se podría haber hecho una elección diferente, pero simplemente no lo fue.


Me temo que no es tanto un debate de objetos de primera o segunda clase, sino una discusión religiosa de buenas prácticas y prácticas aplicables para aplicaciones integradas.

Devolver una estructura significa que una estructura raíz está siendo modificada por el sigilo en las profundidades de la secuencia de llamadas, o una duplicación de datos y el paso de grandes trozos de datos duplicados. Las aplicaciones principales de C todavía se concentran en gran medida alrededor de las aplicaciones integradas profundas. En estos dominios tiene procesadores pequeños que no necesitan pasar grandes bloques de datos. También tiene una práctica de ingeniería que requiere la necesidad de poder operar sin la asignación dinámica de RAM, y con una pila mínima y, a menudo, sin montón. Se podría argumentar que el retorno de la estructura es lo mismo que la modificación mediante el puntero, pero abstraído en sintaxis ... Me temo que argumentaría que no está en la filosofía C de "lo que ves es lo que obtienes" en la De la misma manera es un puntero a un tipo.

Personalmente, diría que ha encontrado un agujero de bucle, ya sea aprobado por la norma o no. C está diseñado de tal manera que la asignación es explícita. Como cuestión de buenas prácticas, usted pasa a los objetos del tamaño de un bus de dirección, normalmente en un ciclo aspiracional, refiriéndose a la memoria que se ha asignado explícitamente en un tiempo controlado dentro del área de desarrolladores. Esto tiene sentido en términos de eficiencia de código, eficiencia de ciclo y ofrece el mayor control y claridad de propósito. Me temo que, en la inspección de códigos, desecharía una función que devolviera una estructura como una mala práctica. C no hace cumplir muchas reglas, es un lenguaje para ingenieros profesionales de muchas maneras, ya que depende del usuario que aplica su propia disciplina. Solo porque puede, no significa que deba ... Ofrece algunas formas bastante a prueba de balas para manejar datos de tamaño y tipo muy complejos utilizando el rigor de tiempo de compilación y minimizando las variaciones dinámicas de la huella y en tiempo de ejecución.





c