Perché C e C ++ supportano l'assegnazione a livello di membro di array all'interno di strutture, ma non in generale?


87

Comprendo che l'assegnazione di array a livello di membro non è supportata, in modo tale che quanto segue non funzionerà:

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

L'ho semplicemente accettato come un dato di fatto, immaginando che lo scopo del linguaggio sia fornire un framework aperto e lasciare che l'utente decida come implementare qualcosa come la copia di un array.

Tuttavia, quanto segue funziona:

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

L'array num[3]è assegnato in base ai membri dalla sua istanza in struct1, nella sua istanza in struct2.

Perché l'assegnazione degli array in base ai membri è supportata per le strutture, ma non in generale?

modifica : il commento di Roger Pate nel thread std :: string in struct - Problemi di copia / assegnazione? sembra puntare nella direzione generale della risposta, ma non ne so abbastanza per confermarlo da solo.

modifica 2 : molte risposte eccellenti. Ho scelto Luther Blissett perché principalmente mi chiedevo quale fosse la logica filosofica o storica dietro il comportamento, ma anche il riferimento di James McNellis alla relativa documentazione delle specifiche è stato utile.


6
Sto facendo in modo che questo abbia sia C che C ++ come tag, perché proviene da C. Inoltre, buona domanda.
GManNickG

4
Potrebbe valere la pena notare che molto tempo fa in C, l'assegnazione della struttura non era generalmente possibile e dovevi usare memcpy()o simili.
ggg

Solo un po 'di FYI ... boost::array( boost.org/doc/libs/release/doc/html/array.html ) e ora std::array( en.cppreference.com/w/cpp/container/array ) sono alternative compatibili con STL al vecchi array C disordinati. Supportano l'assegnazione di copie.
Emile Cormier

@EmileCormier E sono - tada! - strutture attorno agli array.
Peter - Ripristina Monica il

Risposte:


46

Ecco la mia opinione:

Lo sviluppo del linguaggio C offre alcune informazioni sull'evoluzione del tipo di array in C:

Proverò a delineare la cosa dell'array:

I precursori di C B e BCPL non avevano un tipo di array distinto, una dichiarazione come:

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

dichiarerebbe che V è un puntatore (non tipizzato) inizializzato per puntare a una regione inutilizzata di 10 "parole" di memoria. B già utilizzato *per la dereferenziazione del puntatore e aveva la [] notazione a mano abbreviata, *(V+i)intesa V[i]proprio come in C / C ++ oggi. Tuttavia, Vnon è un array, è ancora un puntatore che deve puntare a un po 'di memoria. Ciò ha causato problemi quando Dennis Ritchie ha cercato di estendere B con i tipi di struttura. Voleva che gli array facessero parte delle strutture, come in C oggi:

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

Ma con il concetto B, BCPL di array come puntatori, ciò avrebbe richiesto che il namecampo contenesse un puntatore che doveva essere inizializzato in fase di esecuzione su una regione di memoria di 14 byte all'interno della struttura. Il problema di inizializzazione / layout è stato infine risolto dando agli array un trattamento speciale: il compilatore avrebbe tracciato la posizione degli array nelle strutture, nello stack ecc. Senza effettivamente richiedere che il puntatore ai dati si materializzasse, tranne che nelle espressioni che coinvolgono gli array. Questo trattamento ha permesso a quasi tutto il codice B di funzionare ancora ed è la fonte della regola "gli array si convertono in puntatori se li si guarda" . È un trucco di compatibilità, che si è rivelato molto utile, perché consentiva array di dimensioni aperte ecc.

Ed ecco la mia ipotesi sul motivo per cui l'array non può essere assegnato: poiché gli array erano puntatori in B, potresti semplicemente scrivere:

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

per rebase un "array". Questo ora non aveva più senso, perché la base di una variabile array non era più un lvalue. Quindi questa assegnazione non è stata consentita, il che ha aiutato a catturare i pochi programmi che hanno fatto questo rebasing su array dichiarati. E poi questa nozione è rimasta bloccata: poiché gli array non sono mai stati progettati per essere citati in prima classe dal sistema di tipo C, sono stati per lo più trattati come bestie speciali che diventano puntatori se vengono utilizzati. E da un certo punto di vista (che ignora che gli array C sono un trucco mal riuscito), non consentire l'assegnazione di array ha ancora un senso: un array aperto o un parametro di funzione di array viene trattato come un puntatore senza informazioni sulla dimensione. Il compilatore non dispone delle informazioni per generare un'assegnazione di matrice per loro e l'assegnazione del puntatore era richiesta per motivi di compatibilità.

/* 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
}

Ciò non cambiò quando una revisione di C nel 1978 aggiunse l'assegnazione della struttura ( http://cm.bell-labs.com/cm/cs/who/dmr/cchanges.pdf ). Anche se i record erano tipi distinti in C, non era possibile assegnarli all'inizio di K&R C. Bisognava copiarli a livello di membro con memcpy e si potevano passare solo puntatori ad essi come parametri di funzione. L'assegnazione (e il passaggio di parametri) era ora semplicemente definita come la memcpy della memoria grezza della struttura e poiché questo non poteva rompere il codice esistente, veniva prontamente adattata. Come effetto collaterale non intenzionale, questo ha implicitamente introdotto una sorta di assegnazione di array, ma è successo da qualche parte all'interno di una struttura, quindi questo non poteva davvero introdurre problemi con il modo in cui venivano usati gli array.


È un int[10] c;peccato che il C non abbia definito una sintassi, ad esempio per fare in modo che lvalue si ccomporti come un array di dieci elementi, piuttosto che come un puntatore al primo elemento di un array di dieci elementi. Ci sono alcune situazioni in cuièutile essere in grado di creare un typedef che alloca spazio quando viene usato per una variabile, ma passa un puntatore quando viene usato come argomento di una funzione, ma l'incapacità di avere un valore di tipo array è una significativa debolezza semantica nella lingua.
supercat

Invece di dire "puntatore che deve puntare a un po 'di memoria", il punto importante è che il puntatore stesso deve essere immagazzinato in memoria come un normale puntatore. Questo si riscontra nella tua spiegazione successiva, ma penso che evidenzi meglio la differenza chiave. (Nel C moderno, il nome di una variabile di matrice si riferisce a un blocco di memoria, quindi non è questa la differenza. È che il puntatore stesso non è logicamente memorizzato da nessuna parte nella macchina astratta.)
Peter Cordes

Vedi l'avversione di C per gli array per un bel riassunto della storia.
Peter Cordes

31

Per quanto riguarda gli operatori di assegnazione, lo standard C ++ dice quanto segue (C ++ 03 §5.17 / 1):

Ci sono diversi operatori di assegnazione ... tutti richiedono un lvalue modificabile come operando sinistro

Un array non è un lvalue modificabile.

Tuttavia, l'assegnazione a un oggetto di tipo classe è definita in modo speciale (§5.17 / 4):

L'assegnazione agli oggetti di una classe è definita dall'operatore di assegnazione della copia.

Quindi, guardiamo per vedere cosa fa l'operatore di assegnazione della copia dichiarato implicitamente per una classe (§12.8 / 13):

L'operatore di assegnazione copia definito in modo implicito per la classe X esegue l'assegnazione in senso membro dei suoi suboggetti. ... Ogni sottooggetto viene assegnato nel modo appropriato al suo tipo:
...
- se il sottooggetto è un array, ogni elemento viene assegnato, nel modo appropriato al tipo di elemento
...

Quindi, per un oggetto di tipo classe, gli array vengono copiati correttamente. Nota che se fornisci un operatore di assegnazione copia dichiarato dall'utente, non puoi trarne vantaggio e dovrai copiare l'array elemento per elemento.


Il ragionamento è simile in C (C99 §6.5.16 / 2):

Un operatore di assegnazione deve avere un valore modificabile come operando sinistro.

E §6.3.2.1 / 1:

Un lvalue modificabile è un lvalue che non ha un tipo di array ... [seguono altri vincoli]

In C, l'assegnazione è molto più semplice che in C ++ (§6.5.16.1 / 2):

Nell'assegnazione semplice (=), il valore dell'operando destro viene convertito nel tipo di espressione di assegnazione e sostituisce il valore memorizzato nell'oggetto designato dall'operando sinistro.

Per l'assegnazione di oggetti di tipo struct, gli operandi sinistro e destro devono avere lo stesso tipo, quindi il valore dell'operando destro viene semplicemente copiato nell'operando sinistro.


1
Perché gli array sono immutabili? O meglio, perché l'assegnazione non è definita appositamente per gli array come quando è in un tipo di classe?
GManNickG

1
@ GMan: Questa è la domanda più interessante, non è vero. Per C ++ la risposta è probabilmente "perché è così in C" e per C, immagino sia dovuto solo a come si è evoluto il linguaggio (cioè, il motivo è storico, non tecnico), ma non ero vivo quando la maggior parte di ciò ha avuto luogo, quindi lascerò che sia qualcuno più esperto a rispondere a quella parte :-P (FWIW, non riesco a trovare nulla nei documenti razionali C90 o C99).
James McNellis

2
Qualcuno sa dove si trova la definizione di "valore modificabile" nello standard C ++ 03? Si dovrebbe essere in §3.10. L'indice dice che è definito su quella pagina, ma non lo è. La nota (non normativa) al §8.3.4 / 5 dice "Gli oggetti dei tipi di array non possono essere modificati, vedere 3.10," ma §3.10 non usa una sola volta la parola "array".
James McNellis

@ James: stavo solo facendo lo stesso. Sembra riferirsi a una definizione rimossa. E sì, ho sempre voluto sapere la vera ragione dietro tutto questo, ma sembra un mistero. Ho sentito cose come "impedire alle persone di essere inefficienti assegnando accidentalmente array", ma è ridicolo.
GManNickG

1
@GMan, James: Recentemente c'è stata una discussione su comp.lang.c ++ groups.google.com/group/comp.lang.c++/browse_frm/thread/… se ti sei perso e sei ancora interessato. Apparentemente non è perché un array non è un lvalue modificabile (un array è certamente un lvalue e tutti i lvalue non cost sono modificabili), ma perché =richiede un rvalue su RHS e un array non può essere un rvalue ! La conversione da lvalue a rvalue è vietata per gli array, sostituita con lvalue-to-pointer. static_castnon è più bravo a fare un rvalue perché è definito negli stessi termini.
Potatoswatter

2

In questo link: http://www2.research.att.com/~bs/bs_faq2.html c'è una sezione sull'assegnazione degli array:

I due problemi fondamentali con gli array sono questo

  • un array non conosce la propria dimensione
  • il nome di un array si converte in un puntatore al suo primo elemento alla minima provocazione

E penso che questa sia la differenza fondamentale tra array e strutture. Una variabile di matrice è un elemento di dati di basso livello con una conoscenza di sé limitata. Fondamentalmente, è un pezzo di memoria e un modo per indicizzarlo.

Quindi, il compilatore non può dire la differenza tra int a [10] e int b [20].

Le strutture, tuttavia, non hanno la stessa ambiguità.


3
Quella pagina parla del passaggio di array alle funzioni (cosa che non può essere eseguita, quindi è solo un puntatore, che è ciò che intende quando dice che perde la sua dimensione). Ciò non ha nulla a che fare con l'assegnazione di array agli array. E no, una variabile di array non è solo "realmente" un puntatore al primo elemento, è un array. Gli array non sono puntatori.
GManNickG

Grazie per il commento, ma quando ho letto quella sezione dell'articolo dice in anticipo che gli array non conoscono la propria dimensione, quindi usa un esempio in cui gli array vengono passati come argomenti per illustrare questo fatto. Quindi, quando gli array vengono passati come argomenti, hanno perso le informazioni sulla loro dimensione o non hanno mai avuto le informazioni per cominciare. Ho pensato a quest'ultimo.
Scott Turley

3
Il compilatore può dire la differenza tra i due array di dimensioni diverse - provare a stampare sizeof(a)vs. sizeof(b)o passando aa void f(int (&)[20]);.
Georg Fritzsche

È importante capire che ogni dimensione di array costituisce il proprio tipo. Le regole per il passaggio dei parametri assicurano che sia possibile scrivere funzioni "generiche" di poveri che accettano argomenti di array di qualsiasi dimensione, a scapito della necessità di passare la dimensione separatamente. Se non fosse così (e in C ++ puoi - e devi! - definire parametri di riferimento per array di dimensioni specifiche), avresti bisogno di una funzione specifica per ogni dimensione diversa, chiaramente senza senso. Ne ho scritto in un altro post .
Peter - Ripristina Monica il

0

Lo so, tutti quelli che hanno risposto sono esperti in C / C ++. Ma ho pensato, questo è il motivo principale.

num2 = num1;

Qui stai cercando di modificare l'indirizzo di base dell'array, il che non è consentito.

e, naturalmente, struct2 = struct1;

In questo caso, l'oggetto struct1 viene assegnato a un altro oggetto.


E l'assegnazione di strutture alla fine assegnerà il membro dell'array, che pone esattamente la stessa domanda. Perché uno è consentito e non l'altro, quando è un array in entrambe le situazioni?
GManNickG

1
Concordato. Ma il primo è impedito dal compilatore (num2 = num1). Il secondo non è impedito dal compilatore. Ciò fa un'enorme differenza.
nsivakr

Se gli array fossero assegnabili, si num2 = num1sarebbero comportati perfettamente. Gli elementi di num2avrebbero lo stesso valore dell'elemento corrispondente di num1.
juanchopanza

0

Un altro motivo per cui non sono stati fatti ulteriori sforzi per rinforzare gli array in C è probabilmente che l'assegnazione degli array non sarebbe così utile. Anche se può essere facilmente ottenuto in C avvolgendolo in una struttura (e l'indirizzo della struttura può essere semplicemente convertito all'indirizzo dell'array o anche all'indirizzo del primo elemento dell'array per un'ulteriore elaborazione) questa funzione è usata raramente. Uno dei motivi è che gli array di dimensioni diverse sono incompatibili, il che limita i vantaggi dell'assegnazione o, in relazione, del passaggio alle funzioni in base al valore.

La maggior parte delle funzioni con parametri di array nei linguaggi in cui gli array sono tipi di prima classe sono scritte per array di dimensioni arbitrarie. La funzione quindi di solito itera su un dato numero di elementi, un'informazione fornita dall'array. (In C l'idioma è, ovviamente, passare un puntatore e un conteggio di elementi separati.) Una funzione che accetta un array di una sola dimensione specifica non è necessaria spesso, quindi non manca molto. (Questo cambia quando puoi lasciare che sia il compilatore a generare una funzione separata per qualsiasi dimensione dell'array che si verifica, come con i modelli C ++; questo è il motivo per cui std::arrayè utile.)

Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.