Perché C non ha matrici di prima classe? [duplicare]

4

Ho una domanda sulla semantica di C e mi chiedevo perché i poteri si decidessero a fare matrici diverse da ogni altro tipo nella lingua.

Comprendo che array e puntatori sono diverse astrazioni in fase di compilazione in C. Una matrice ha una lunghezza fissa e sizeof restituisce la lunghezza della falcata * per una matrice. Per un puntatore restituisce semplicemente la larghezza del puntatore stesso. Tuttavia, gli array non possono essere passati come valori in funzioni o restituiti direttamente dalle funzioni. In effetti mi chiedo perché non puoi fare l'equivalente di quanto segue.

int sum(int array[10]) {
    const size_t length = sizeof(array)/sizeof(array[0]);
    int out = 0;
    for (int i = 0; i < length; i++) {
        out += array[i];
    }
    return out;
}

Sembra invece che gli array siano convertiti implicitamente in puntatori quando vengono passati o restituiti da funzioni e la lunghezza dell'array viene semplicemente scartata, impedendo in tal modo alle informazioni sulla lunghezza dell'array di lasciare sempre l'oscilloscopio dell'array dichiarato in.

Ci sarebbero degli effetti collaterali imprevedibili di estendere la lingua per consentire il passaggio degli array dentro e fuori le funzioni in questo modo, a parte forse la rottura della compatibilità con le versioni precedenti.

    
posta Gregory Nisbet 16.03.2016 - 18:50
fonte

2 risposte

3

Questa decisione di progettazione deve essere inserita nel contesto storico. C è stato creato per implementare UNIX su un PDP11 , una CPU a 16 bit con meno potenza di calcolo rispetto a qualsiasi smartphone di oggi, e max 4 MB di memoria. L'ottimizzazione dei compilatori era molto meno avanzata di oggi. L'implementazione di un sistema operativo su tali macchine ha richiesto di evitare sovraccarichi non necessari e, poiché questo era lo scopo principale del linguaggio, non è sorprendente che le funzionalità linguistiche favoriscano le prestazioni.

Il passaggio di una matrice di dimensioni note per valore richiederebbe di spostare tutto il contenuto dell'array nello stack. Questa funzione incoraggerebbe il consumo di memoria, quando la memoria era scarsa e lo spostamento della memoria era lento. Passare un array di dimensioni variabili avrebbe richiesto di spingere la dimensione e il contenuto e calcolare dinamicamente dove trovare gli argomenti rimanenti nello stack. Un sovraccarico inaccettabile per uno sviluppatore di sistemi operativi!

Un altro fattore importante è che è stato preferito passare argomenti nei registri della CPU anziché in pila. A quel tempo, la parola chiave register era importante per ottimizzare l'ottimizzazione del codice. Era prassi comune dichiarare argomenti di funzione come variabili di registro, per evitare operazioni push / pop. Fortunatamente, puoi passare facilmente i puntatori tramite i registri ma non i valori lunghi come gli array.

Tutto questo era un motivo sufficiente per passare qualsiasi cosa tranne i tipi di build-in per indirizzo invece che per valore. Questo è stato anche il motivo per il vincolo che passa per indirizzo per ogni struttura nel K & R. Iniziale

Naturalmente, oggigiorno, nessuno usa più register . I COmpiler hanno sovraperformato gli umani nell'uso astuto di questa risorsa. La parola chiave potrebbe essere mantenuta per compatibilità con le versioni precedenti, consentendo una transizione facile.

Sfortunatamente, il passaggio di matrici per valore interromperà la compatibilità all'indietro, poiché la semantica corrente viene utilizzata in milioni di righe di codice. Ad esempio, cosa faresti con:

int a[10]; 
foo (a, 10);    // work on whole array
foo (a+1, 9);   // work on part of the array
foo (a+2, 3);   // work on a reduced part of the array
foo (a,3);      // work on the three first elements

Se prendiamo l'ultimo esempio, non è facile definire un valore passante semantico. Se scrivessi foo (a [3]), sarebbe in conflitto con il modo attuale di passare il 4 ° int di un array.

Se ti piace passare gli array in base al valore, esistono 2 opzioni:

1) la limitazione del passaggio della struttura per indirizzo non è più in vigore (penso che sia dal C99). Quindi puoi fare perfettamente il seguente:

struct wrapper { 
    int a[10]; 
}; 

void foo(struct wrapper w);  // pass the whole struct including the array by value. 

2) usa C ++ con i vettori, che abilitano tutto ciò che si può sognare di matrici di prima classe.

    
risposta data 16.03.2016 - 21:21
fonte
2

È meno ovvio ciò che questo ipotetico codice dovrebbe significare. (Ho corretto un refuso nel tuo ciclo for e ho cambiato il tipo di indice in size_t .)

// Hypothetical code, does not work
int sum(int array[10]) {
    const size_t length = sizeof(array) / sizeof(array[0]);
    int out = 0;
    for (size_t i = 0; i < length; ++i) {
        out += array[i];
    }
    return out;
}

Un'interpretazione sarebbe che sum accetta un array di 10% diint s in base al valore. (In tal caso, perché calcolare nuovamente la dimensione utilizzando sizeof(array)/sizeof(array[0]) ? È già codificato a 10.) Ma perché ti piacerebbe una funzione che riassumi gli elementi di una matrice da chiamare con una copia dell'array? E se non vuoi fare una copia, C ti consente già di farlo.

// Valid C99 code, does what you expect
int sum(const int (*array)[10]) {
  int out = 0;
  for (size_t i = 0; i < 10; ++i) {
    out += (*array)[i];
  }
  return out;
}

Ma probabilmente non intendevi codificare a macchina la dimensione dell'array. (All'inizio Pascal aveva questo e C hacker del tempo odiato per questo stesso motivo.)

Se vuoi essere in grado di chiamare la funzione con una matrice di qualsiasi dimensione e avere accesso a quella dimensione all'interno della funzione, in qualche modo questa informazione deve essere passata. Questo porta a problemi.

  • Il compilatore dovrebbe generare codice per passare un parametro aggiuntivo (la dimensione dell'array) alla funzione. Ciò violerebbe la filosofia "zero overhead".
  • Se vuoi passare la semantica del valore-da-oggetto, chiamare una funzione diventa ancora più complicato perché il lato chiamato non sa quanti argomenti è stato passato. Ovviamente potrebbe ispezionare l'argomento della lunghezza passata e quindi capire dove trovare il resto dei dati. Non sto dicendo che questo è impossibile da fare ma molto più complesso delle altre astrazioni fornite da C e non si traduce banalmente in codice assembly per le macchine del tempo.
  • Anche se il compilatore ha generato il codice per passare la dimensione dell'array alla funzione in fase di esecuzione, l'espressione sizeof non funzionerebbe ancora perché l'operatore sizeof valuta una costante di compilazione che deve essere lo stesso per tutte le chiamate alla funzione. Quindi avremmo bisogno di un operatore aggiuntivo per interrogare la dimensione di un array passato come parametro. Ciò significherebbe ancora più complessità.

Sembra anche che non ci sia molto bisogno di una tale funzionalità. Se vuoi passare un array di riferimento per lunghezza arbitrario,

int sum(const int * array, size_t length);
l'idioma

fa proprio questo. E se vuoi passare un valore-specifico dell'array a dimensione fissa, può essere facilmente emulato tramite struct s.

struct coordinate
{
  double xyz[3];
};

double norm(struct coordinate c);

In C ++, template s consente di spostare le informazioni di runtime in fase di compilazione.

// Valid C++98, does what you really expect
template <typename T, std::size_t N>
T sum(const T (&array)[N]) {
  T out = 0;
  for (std::size_t i = 0; i < N; ++i) {
      out += array[i];
  }
  return out;
}

Qui, N è una costante fissa quando sum è istanziata e non è necessario che venga passato come parametro di runtime aggiuntivo. Ma a meno che C ottenga un equivalente a template s in C ++, il concetto di passare array nativi a funzioni non si adatta naturalmente al linguaggio.

    
risposta data 16.03.2016 - 21:21
fonte

Leggi altre domande sui tag