¿Por qué C y C ++ admiten la asignación de matrices por miembros dentro de estructuras, pero no en general?

87

Entiendo que no se admite la asignación de matrices por miembros, por lo que lo siguiente no funcionará:

int num1[3] = {1,2,3};
int num2[3];
num2 = num1; // "error: invalid array assignment"

Simplemente acepté esto como un hecho, pensando que el objetivo del lenguaje es proporcionar un marco de trabajo abierto y dejar que el usuario decida cómo implementar algo como la copia de una matriz.

Sin embargo, lo siguiente funciona:

struct myStruct { int num[3]; };
struct myStruct struct1 = {{1,2,3}};
struct myStruct struct2;
struct2 = struct1;

La matriz num[3]se asigna por miembros desde su instancia enstruct1 , a su instancia en struct2.

¿Por qué se admite la asignación de matrices por miembros para estructuras, pero no en general?

editar : Comentario de Roger Pate en el hilo std :: string in struct - ¿Problemas de copia / asignación? parece apuntar en la dirección general de la respuesta, pero no sé lo suficiente para confirmarlo yo mismo.

edición 2 : Muchas respuestas excelentes. Elegí el de Luther Blissett porque principalmente me preguntaba acerca de la razón filosófica o histórica detrás del comportamiento, pero la referencia de James McNellis a la documentación de especificaciones relacionada también fue útil.

ozmo
fuente
6
Estoy haciendo que esto tenga tanto C como C ++ como etiquetas, porque esto se origina en C. Además, buena pregunta.
GManNickG
4
Podría valer la pena señalar que hace mucho tiempo en C, la asignación de estructura generalmente no era posible y tenía que usar memcpy()o similar.
ggg
Solo un poco para su información ... boost::array( boost.org/doc/libs/release/doc/html/array.html ) y ahora std::array( en.cppreference.com/w/cpp/container/array ) son alternativas compatibles con STL al matrices C antiguas desordenadas. Admiten la asignación de copias.
Emile Cormier
@EmileCormier Y lo son - ¡tada! - estructuras alrededor de matrices.
Peter - Reincorpora a Monica

Respuestas:

46

Esta es mi opinión al respecto:

El desarrollo del lenguaje C ofrece una idea de la evolución del tipo de matriz en C:

Intentaré delinear lo de la matriz:

Los precursores B y BCPL de C no tenían un tipo de matriz distinto, una declaración como:

auto V[10] (B)
or 
let V = vec 10 (BCPL)

declararía que V es un puntero (sin tipo) que se inicializa para apuntar a una región no utilizada de 10 "palabras" de memoria. B ya se usaba *para desreferenciar punteros y tenía la [] notación abreviada, es *(V+i)decir V[i], al igual que en C / C ++ hoy. Sin embargo, Vno es una matriz, sigue siendo un puntero que tiene que apuntar a algo de memoria. Esto causó problemas cuando Dennis Ritchie intentó extender B con tipos de estructura. Quería que las matrices fueran parte de las estructuras, como en C hoy:

struct {
    int inumber;
    char name[14];
};

Pero con el concepto B, BCPL de matrices como punteros, esto habría requerido que el namecampo contuviera un puntero que tenía que inicializarse en tiempo de ejecución a una región de memoria de 14 bytes dentro de la estructura. El problema de inicialización / diseño finalmente se resolvió dándole a las matrices un tratamiento especial: el compilador rastrearía la ubicación de las matrices en estructuras, en la pila, etc.sin requerir realmente que el puntero a los datos se materialice, excepto en expresiones que involucren las matrices. Este tratamiento permitió que casi todo el código B aún se ejecutara y es la fuente de la regla "las matrices se convierten en puntero si las observa" . Es un truco de compatibilidad, que resultó ser muy útil, porque permitía matrices de tamaño abierto, etc.

Y aquí está mi suposición de por qué no se puede asignar una matriz: dado que las matrices eran punteros en B, simplemente podría escribir:

auto V[10];
V=V+5;

para reajustar una "matriz". Esto ahora no tenía sentido, porque la base de una variable de matriz ya no era un lvalue. Así que esta asignación fue rechazada, lo que ayudó a capturar los pocos programas que hicieron este rebase en matrices declaradas.. Y luego esta noción se atascó: como las matrices nunca fueron diseñadas para ser citadas de primera clase del sistema de tipo C, en su mayoría fueron tratadas como bestias especiales que se convierten en punteros si las usa. Y desde cierto punto de vista (que ignora que las matrices C son un truco fallido), rechazar la asignación de matrices todavía tiene algún sentido: una matriz abierta o un parámetro de función de matriz se trata como un puntero sin información de tamaño. El compilador no tiene la información para generar una asignación de matriz para ellos y la asignación de puntero fue requerida por razones de compatibilidad.

/* Example how array assignment void make things even weirder in C/C++, 
   if we don't want to break existing code.
   It's actually better to leave things as they are...
*/
typedef int vec[3];

void f(vec a, vec b) 
{
    vec x,y; 
    a=b; // pointer assignment
    x=y; // NEW! element-wise assignment
    a=x; // pointer assignment
    x=a; // NEW! element-wise assignment
}

Esto no cambió cuando una revisión de C en 1978 agregó la asignación de estructuras ( http://cm.bell-labs.com/cm/cs/who/dmr/cchanges.pdf ). Aunque los registros fueron tipos distintos en C, no era posible asignarlos en K&R C. Tenías que copiarlos por miembros con memcpy y solo podías pasarles punteros como parámetros de función. La asignación (y el paso de parámetros) ahora se definió simplemente como la memoria en bruto de la estructura de la estructura y, dado que esto no podía romper el código existente, se adaptó fácilmente. Como efecto secundario involuntario, esto introdujo implícitamente algún tipo de asignación de matriz, pero esto sucedió en algún lugar dentro de una estructura, por lo que esto realmente no podría presentar problemas con la forma en que se usaban las matrices.

Mainframe nórdico
fuente
Es una lástima que C no definió una sintaxis, por ejemplo, int[10] c;para hacer que lvalue se ccomporte como una matriz de diez elementos, en lugar de como un puntero al primer elemento de una matriz de diez elementos. Hay algunas situaciones en las que es útil poder crear una typedef que asigna espacio cuando se usa para una variable, pero pasa un puntero cuando se usa como un argumento de función, pero la incapacidad de tener un valor de tipo de matriz es una debilidad semántica significativa en el idioma.
supercat
En lugar de decir "puntero que debe apuntar a algo de memoria", lo importante es que el puntero en sí debe almacenarse en la memoria como un puntero normal. Esto aparece en su explicación posterior, pero creo que resalta mejor la diferencia clave. (En C moderno, el nombre de una variable de matriz se refiere a un bloque de memoria, por lo que esa no es la diferencia. Es que el puntero en sí no se almacena lógicamente en ninguna parte de la máquina abstracta)
Peter Cordes
Consulte la aversión de C a las matrices para obtener un buen resumen de la historia.
Peter Cordes
31

Con respecto a los operadores de asignación, el estándar C ++ dice lo siguiente (C ++ 03 §5.17 / 1):

Hay varios operadores de asignación ... todos requieren un lvalue modificable como su operando izquierdo

Una matriz no es un lvalor modificable.

Sin embargo, la asignación a un objeto de tipo de clase se define especialmente (§5.17 / 4):

La asignación a los objetos de una clase la define el operador de asignación de copia.

Entonces, miramos para ver qué hace el operador de asignación de copia declarado implícitamente para una clase (§12.8 / 13):

El operador de asignación de copia definido implícitamente para la clase X realiza la asignación por miembros de sus subobjetos. ... Cada subobjeto se asigna de la manera apropiada a su tipo:
...
- si el subobjeto es una matriz, cada elemento se asigna, de la manera apropiada al tipo de elemento
...

Entonces, para un objeto de tipo de clase, las matrices se copian correctamente. Tenga en cuenta que si proporciona un operador de asignación de copia declarado por el usuario, no puede aprovechar esto y tendrá que copiar la matriz elemento por elemento.


El razonamiento es similar en C (C99 §6.5.16 / 2):

Un operador de asignación tendrá un lvalue modificable como su operando izquierdo.

Y §6.3.2.1 / 1:

Un lvalue modificable es un lvalue que no tiene tipo de matriz ... [siguen otras restricciones]

En C, la asignación es mucho más simple que en C ++ (§6.5.16.1 / 2):

En la asignación simple (=), el valor del operando derecho se convierte al tipo de expresión de asignación y reemplaza el valor almacenado en el objeto designado por el operando izquierdo.

Para la asignación de objetos de tipo estructura, los operandos izquierdo y derecho deben tener el mismo tipo, por lo que el valor del operando derecho simplemente se copia en el operando izquierdo.

James McNellis
fuente
1
¿Por qué las matrices son inmutables? O más bien, ¿por qué la asignación no se define especialmente para matrices como cuando está en un tipo de clase?
GManNickG
1
@GMan: Esa es la pregunta más interesante, ¿no? Para C ++, la respuesta es probablemente "porque así es en C", y para C, supongo que se debe a cómo evolucionó el lenguaje (es decir, la razón es histórica, no técnica), pero yo no estaba vivo. cuando ocurrió la mayor parte de eso, dejaré que alguien con más conocimientos responda esa parte :-P (FWIW, no puedo encontrar nada en los documentos de justificación C90 o C99).
James McNellis
2
¿Alguien sabe dónde está la definición de "valor modificable" en el estándar C ++ 03? Se debe estar en §3.10. El índice dice que está definido en esa página, pero no lo está. La nota (no normativa) en §8.3.4 / 5 dice "Los objetos de tipos de matriz no se pueden modificar, ver 3.10", pero §3.10 no usa una sola vez la palabra "matriz".
James McNellis
@James: Solo estaba haciendo lo mismo. Parece referirse a una definición eliminada. Y sí, siempre he querido saber la verdadera razón detrás de todo esto, pero parece un misterio. He escuchado cosas como "evitar que las personas sean ineficientes asignando matrices accidentalmente", pero eso es ridículo.
GManNickG
1
@GMan, James: Recientemente hubo una discusión sobre comp.lang.c ++ groups.google.com/group/comp.lang.c++/browse_frm/thread/… si te lo perdiste y aún estás interesado. Aparentemente, no es porque una matriz no sea un valor l modificable (una matriz ciertamente es un valor l y todos los valores l no constantes son modificables), sino porque =requiere un valor r en el RHS y una matriz no puede ser un valor r . La conversión de lvalue a rvalue está prohibida para matrices, reemplazada por lvalue-to-pointer. static_castno es mejor para hacer un rvalue porque está definido en los mismos términos.
Potatoswatter
2

En este enlace: http://www2.research.att.com/~bs/bs_faq2.html hay una sección sobre asignación de matrices:

Los dos problemas fundamentales con las matrices son que

  • una matriz no conoce su propio tamaño
  • el nombre de una matriz se convierte en un puntero a su primer elemento a la menor provocación

Y creo que esta es la diferencia fundamental entre matrices y estructuras. Una variable de matriz es un elemento de datos de bajo nivel con un autoconocimiento limitado. Básicamente, es un trozo de memoria y una forma de indexarlo.

Por tanto, el compilador no puede diferenciar entre int a [10] e int b [20].

Las estructuras, sin embargo, no tienen la misma ambigüedad.

Scott Turley
fuente
3
Esa página habla de pasar matrices a funciones (lo que no se puede hacer, por lo que es solo un puntero, que es lo que quiere decir cuando dice que pierde su tamaño). Eso no tiene nada que ver con la asignación de matrices a matrices. Y no, una variable de matriz no es sólo "realmente" un puntero al primer elemento, es una matriz. Las matrices no son punteros.
GManNickG
Gracias por el comentario, pero cuando leí esa sección del artículo, dice desde el principio que las matrices no conocen su propio tamaño, luego usa un ejemplo en el que las matrices se pasan como argumentos para ilustrar ese hecho. Entonces, cuando las matrices se pasan como argumentos, ¿perdieron la información sobre su tamaño o nunca tuvieron la información para empezar? Asumí lo último.
Scott Turley
3
El compilador puede decir la diferencia entre dos matrices de diferentes tamaños - intente imprimir sizeof(a)vs. sizeof(b)o pasar aa void f(int (&)[20]);.
Georg Fritzsche
Es importante comprender que cada tamaño de matriz constituye su propio tipo. Las reglas para el paso de parámetros aseguran que pueda escribir funciones "genéricas" de los pobres que toman argumentos de matriz de cualquier tamaño, a expensas de tener que pasar el tamaño por separado. Si ese no fuera el caso (y en C ++ usted puede - ¡y debe! - definir parámetros de referencia para matrices de tamaño específico), necesitaría una función específica para cada tamaño diferente, claramente una tontería. Escribí sobre eso en otra publicación .
Peter - Reincorpora a Monica
0

Lo sé, todos los que respondieron son expertos en C / C ++. Pero pensé, esta es la razón principal.

num2 = num1;

Aquí está intentando cambiar la dirección base de la matriz, lo cual no está permitido.

y por supuesto, struct2 = struct1;

Aquí, el objeto struct1 se asigna a otro objeto.

nsivakr
fuente
Y la asignación de estructuras eventualmente asignará el miembro de la matriz, lo que plantea exactamente la misma pregunta. ¿Por qué se permite uno y no el otro, cuando es una matriz en ambas situaciones?
GManNickG
1
Convenido. Pero el compilador evita el primero (num2 = num1). El compilador no evita el segundo. Eso hace una gran diferencia.
nsivakr
Si las matrices fueran asignables, num2 = num1se comportarían perfectamente. Los elementos de num2tendrían el mismo valor que el elemento correspondiente de num1.
juanchopanza
0

Otra razón por la que no se hicieron más esfuerzos para reforzar las matrices en C es probablemente que la asignación de matrices no sería tan útil. Aunque se puede lograr fácilmente en C envolviéndolo en una estructura (y la dirección de la estructura se puede convertir simplemente a la dirección de la matriz o incluso a la dirección del primer elemento de la matriz para su procesamiento posterior), esta característica rara vez se usa. Una razón es que las matrices de diferentes tamaños son incompatibles, lo que limita los beneficios de la asignación o, relacionado, el paso a funciones por valor.

La mayoría de las funciones con parámetros de matriz en lenguajes donde las matrices son tipos de primera clase se escriben para matrices de tamaño arbitrario. Luego, la función generalmente itera sobre el número dado de elementos, una información que proporciona la matriz. (En C, el modismo es, por supuesto, pasar un puntero y un recuento de elementos separados.) Una función que acepta una matriz de un solo tamaño específico no es necesaria con tanta frecuencia, por lo que no se pierde mucho. (Esto cambia cuando puede dejar que el compilador genere una función separada para cualquier tamaño de matriz que ocurra, como con las plantillas de C ++; esta es la razón por la que std::arrayes útil).

Peter - Reincorporar a Monica
fuente