Perché gli array a lunghezza variabile non fanno parte dello standard C ++?


326

Non ho usato molto C negli ultimi anni. Quando ho letto questa domanda oggi mi sono imbattuto in una sintassi C che non conoscevo.

Apparentemente in C99 è valida la seguente sintassi:

void foo(int n) {
    int values[n]; //Declare a variable length array
}

Questa sembra una funzionalità piuttosto utile. Si è mai discusso dell'aggiunta allo standard C ++ e, in caso affermativo, perché è stato omesso?

Alcuni potenziali motivi:

  • Peloso per i fornitori di compilatori da implementare
  • Incompatibile con qualche altra parte dello standard
  • La funzionalità può essere emulata con altri costrutti C ++

Lo standard C ++ afferma che la dimensione dell'array deve essere un'espressione costante (8.3.4.1).

Sì, ovviamente mi rendo conto che nell'esempio giocattolo si potrebbe usare std::vector<int> values(m);, ma questo alloca memoria dall'heap e non dallo stack. E se voglio un array multidimensionale come:

void foo(int x, int y, int z) {
    int values[x][y][z]; // Declare a variable length array
}

la vectorversione diventa piuttosto maldestra:

void foo(int x, int y, int z) {
    vector< vector< vector<int> > > values( /* Really painful expression here. */);
}

Anche le sezioni, le righe e le colonne saranno potenzialmente distribuite su tutta la memoria.

Guardando la discussione comp.std.c++è chiaro che questa domanda è piuttosto controversa con alcuni nomi molto pesanti su entrambi i lati dell'argomento. Non è certo ovvio che a std::vectorsia sempre una soluzione migliore.


3
Solo per curiosità, perché deve essere allocato in pila? Sei così spaventato dai problemi di prestazioni di allocazione dell'heap?
Dimitri C.

32
@Dimitri Non proprio, ma non si può negare che l'allocazione dello stack sarà più veloce dell'allocazione dell'heap. E in alcuni casi questo può avere importanza.
Andreas Brinck,

11
Il vantaggio principale delle matrici a lunghezza variabile è che tutti i dati sono vicini tra loro, quindi quando si scorre attraverso questo array si leggono e si scrivono byte uno accanto all'altro. I tuoi dati vengono recuperati nella cache e la CPU può lavorare su di essa senza recuperare e inviare i byte alla / dalla memoria.
Calmarius,

4
Le matrici a lunghezza variabile possono anche essere usate per sostituire le costanti del preprocessore con variabili const statiche. Anche in C non hai altre opzioni per VLA, e talvolta è necessario scrivere codice C / C ++ portatile (compatibile con entrambi i compilatori).
Yury il

2
a parte questo, sembra che clang ++ consenta VLA.
user3426763

Risposte:


204

Recentemente è stata avviata una discussione al riguardo in usenet: perché nessun VLA in C ++ 0x .

Sono d'accordo con quelle persone che sembrano concordare sul fatto che dover creare un potenziale array di grandi dimensioni nello stack, che di solito ha solo poco spazio disponibile, non è buono. L'argomento è, se si conosce in anticipo la dimensione, è possibile utilizzare un array statico. E se non conosci in anticipo le dimensioni, scriverai un codice non sicuro.

I VLA C99 potrebbero offrire un piccolo vantaggio nel poter creare piccoli array senza sprecare spazio o chiamare costruttori per elementi inutilizzati, ma introdurranno modifiche piuttosto grandi al sistema dei tipi (è necessario essere in grado di specificare i tipi in base ai valori di runtime - questo non esiste ancora nell'attuale C ++, ad eccezione newdegli specificatori di tipi di operatore, ma sono trattati in modo speciale, in modo che il tempo di esecuzione non sfugga all'ambito newdell'operatore).

Puoi usarlo std::vector, ma non è esattamente lo stesso, in quanto utilizza la memoria dinamica e farla utilizzare il proprio allocatore di stack non è esattamente facile (anche l'allineamento è un problema). Inoltre, non risolve lo stesso problema, poiché un vettore è un contenitore ridimensionabile, mentre i VLA sono di dimensioni fisse. La proposta di array dinamico C ++ ha lo scopo di introdurre una soluzione basata su libreria, in alternativa a un VLA basato sul linguaggio. Tuttavia, non farà parte del C ++ 0x, per quanto ne so.


22
+1 e accettato. Un commento però, penso che l'argomento della sicurezza sia un po 'debole poiché ci sono molti altri modi per causare overflow dello stack. L'argomento di sicurezza potrebbe essere utilizzato per supportare la posizione in cui non si dovrebbe mai usare la ricorsione e che è necessario allocare tutti gli oggetti dall'heap.
Andreas Brinck,

17
Quindi stai dicendo che, poiché ci sono altri modi per causare overflow dello stack, potremmo anche incoraggiarne di più?
jalf

3
@Andreas, d'accordo sulla debolezza. Ma per la ricorsione, ci vuole un numero enorme di chiamate fino a quando lo stack non viene consumato e, in tal caso, le persone userebbero l'iterazione. Come dicono alcune persone nel thread di Usenet, tuttavia, questo non è un argomento contro i VLA in tutti i casi, poiché a volte potresti sicuramente conoscere un limite superiore. Ma in quei casi, da quello che vedo un array statico può essere ugualmente sufficiente, dal momento che non sprecherebbe comunque molto spazio (se lo fosse , allora dovresti effettivamente chiedere se l'area dello stack è di nuovo abbastanza grande).
Johannes Schaub - lett

10
Guarda anche la risposta di Matt Austern in quel thread: la specifica del linguaggio dei VLA sarebbe probabilmente molto più complessa per il C ++, a causa delle corrispondenze di tipo più rigorose in C ++ (esempio: C consente di assegnare un T(*)[]a a T(*)[N]- in C ++ ciò non è consentito, poiché C ++ non conosce la "compatibilità dei tipi" - richiede corrispondenze esatte), parametri di tipo, eccezioni, con- distruttori e roba del genere. Non sono sicuro che i benefici degli VLA ripagheranno davvero tutto quel lavoro. Ma poi, non ho mai usato i VLA nella vita reale, quindi probabilmente non conosco buoni casi d'uso per loro.
Johannes Schaub - lett

1
@AHelps: Forse quale sarebbe la soluzione migliore sarebbe un tipo che si comporta in qualche modo come vectorma richiede un modello di utilizzo LIFO fisso e mantiene uno o più buffer allocati staticamente per thread che sono generalmente dimensionati in base alla più grande allocazione totale che il thread ha mai usato, ma che potrebbe essere esplicitamente tagliato. Una normale "allocazione" richiederebbe, nel caso comune, nient'altro che una copia puntatore, sottrazione puntatore da puntatore, confronto di numeri interi e aggiunta puntatore; la disallocazione richiederebbe semplicemente una copia del puntatore. Non molto più lento di un VLA.
supercat

217

(Background: ho una certa esperienza nell'implementazione di compilatori C e C ++.)

Le matrici a lunghezza variabile in C99 erano sostanzialmente un passo falso. Per supportare i VLA, C99 doveva fare le seguenti concessioni al buon senso:

  • sizeof xnon è più sempre una costante di compilazione; il compilatore deve talvolta generare codice per valutare sizeofun'espressione in fase di esecuzione.

  • Permettendo VLA bidimensionali ( int A[x][y]) necessaria una nuova sintassi per le funzioni che accettano 2D VLA come parametri dichiara: void foo(int n, int A[][*]).

  • Ancora meno importante nel mondo C ++, ma estremamente importante per il pubblico target di C di programmatori di sistemi embedded, dichiarare un VLA significa tagliare un pezzo arbitrariamente grande del proprio stack. Questo è uno stack overflow e crash garantito . (Ogni volta che si dichiara int A[n], si sta implicitamente affermando che avete 2GB di stack di riserva. Dopo tutto, se si sa " nè sicuramente inferiore a 1000 qui", quindi si sarebbe solo dichiarare int A[1000]. Sostituendo il numero intero a 32 bit nper 1000è un'ammissione che non hai idea di quale dovrebbe essere il comportamento del tuo programma.)

Bene, passiamo ora a parlare di C ++. In C ++, abbiamo la stessa forte distinzione tra "sistema di tipo" e "sistema di valori" che fa C89 ... ma abbiamo davvero iniziato a fare affidamento su di esso in modi che C non ha. Per esempio:

template<typename T> struct S { ... };
int A[n];
S<decltype(A)> s;  // equivalently, S<int[n]> s;

Se nnon fosse una costante di compilazione (cioè se Afosse di tipo variamente modificato), quale sarebbe il tipo di S? Sarebbe S's tipo anche essere determinata solo in fase di esecuzione?

Che dire di questo:

template<typename T> bool myfunc(T& t1, T& t2) { ... };
int A1[n1], A2[n2];
myfunc(A1, A2);

Il compilatore deve generare il codice per alcune istanze di myfunc. Come dovrebbe essere quel codice? Come possiamo generare staticamente quel codice, se non conosciamo il tipo di A1al momento della compilazione?

Peggio ancora, cosa succede se si scopre in fase di esecuzione che n1 != n2, in modo che !std::is_same<decltype(A1), decltype(A2)>()? In tal caso, la chiamata a myfunc non dovrebbe nemmeno essere compilata , perché la deduzione del tipo di modello non dovrebbe riuscire! Come potremmo eventualmente emulare quel comportamento in fase di esecuzione?

Fondamentalmente, C ++ si sta muovendo nella direzione di spingere sempre più decisioni in fase di compilazione : generazione di codice modello, constexprvalutazione delle funzioni e così via. Nel frattempo, C99 era impegnato a spingere le decisioni tradizionalmente in fase di compilazione (ad es. sizeof) Nel runtime . Con questo in mente, ha davvero senso spendere qualche sforzo nel tentativo di integrare i VLA in stile C99 in C ++?

Come ogni altro risponditore ha già sottolineato, C ++ fornisce molti meccanismi di allocazione dell'heap ( std::unique_ptr<int[]> A = new int[n];o std::vector<int> A(n);essendo quelli ovvi) quando si vuole davvero trasmettere l'idea "Non ho idea di quanta RAM potrei avere bisogno". E C ++ fornisce un elegante modello di gestione delle eccezioni per affrontare l'inevitabile situazione in cui la quantità di RAM necessaria è maggiore della quantità di RAM che hai. Ma spero che questa risposta ti dia una buona idea del perché i VLA in stile C99 non si adattano bene al C ++ e non si adattano nemmeno al C99. ;)


Per ulteriori informazioni sull'argomento, vedere N3810 "Alternative per estensioni di array" , articolo di Bjarne Stroustrup dell'ottobre 2013 sui VLA. Il POV di Bjarne è molto diverso dal mio; N3810 si concentra maggiormente sulla ricerca di una buona sintassi ish C ++ per le cose e sullo scoraggiamento dell'uso di array grezzi in C ++, mentre mi sono concentrato maggiormente sulle implicazioni per la metaprogrammazione e il sistema dei tipi. Non so se considera risolte, risolvibili o semplicemente poco interessanti le implicazioni relative alla metaprogrammazione / tipi di sistema.


Un buon post sul blog che colpisce molti di questi stessi punti è "Uso legittimo di array a lunghezza variabile" (Chris Wellons, 27-10-2019).


15
Sono d'accordo che i VLA avevano torto. alloca()Invece, molto più ampiamente implementato e molto più utile, avrebbe dovuto essere standardizzato in C99. I VLA sono ciò che accade quando un comitato standard salta fuori prima delle implementazioni, anziché viceversa.
MadScientist,

10
Il sistema di tipi a modifica variabile è un'ottima aggiunta all'IMO, e nessuno dei tuoi punti elenco viola il buon senso. (1) lo standard C non distingue tra "tempo di compilazione" e "tempo di esecuzione", pertanto si tratta di un problema non risolto; (2) *È facoltativo, puoi (e dovresti) scrivere int A[][n]; (3) È possibile utilizzare il sistema di tipi senza dichiarare effettivamente alcun VLA. Ad esempio, una funzione può accettare array di tipo variamente modificato e può essere chiamata con array non VLA 2-D di dimensioni diverse. Tuttavia fai punti validi nell'ultima parte del tuo post.
MM

3
"dichiarare un VLA significa tagliare un pezzo arbitrariamente grande del tuo stack. Si tratta di uno stack overflow e di un crash garantiti. (Ogni volta che dichiari int A [n], stai implicitamente affermando che hai 2 GB di stack da risparmiare" è empiricamente falso. Ho appena eseguito un programma VLA con uno stack molto inferiore a 2 GB senza overflow dello stack.
Jeff

3
@Jeff: Qual è stato il valore massimo di nnel tuo caso di test e qual è stata la dimensione del tuo stack? Ti suggerisco di provare a inserire un valore nalmeno grande quanto la dimensione del tuo stack. (E se l'utente non ha modo di controllare il valore di nnel tuo programma, allora ti suggerisco di propagare il valore massimo di nstraight nella dichiarazione: dichiarare int A[1000]o qualunque cosa tu abbia bisogno. I VLA sono solo necessari e solo pericolosi, quando il valore massimo di nnon è limitato da nessuna piccola costante di tempo di compilazione.)
Quuxplusone

2
Poiché alloca () può essere implementato usando tali intrinseci è per definizione vero che alloca () potrebbe essere implementato su qualsiasi piattaforma, come funzione standard del compilatore. Non c'è motivo per cui il compilatore non sia in grado di rilevare la prima istanza di alloca () e di organizzare che i tipi di segni e rilasci siano incorporati nel codice, e non c'è motivo per cui un compilatore non possa implementare alloca () usando l'heap se non può essere fatto con lo stack. Ciò che è duro / non portatile è avere alloca () implementato su un compilatore C, in modo che funzioni attraverso una vasta gamma di compilatori e sistemi operativi.
MadScientist,

26

È sempre possibile utilizzare alloca () per allocare memoria nello stack in fase di esecuzione, se si desidera:

void foo (int n)
{
    int *values = (int *)alloca(sizeof(int) * n);
}

L'allocazione nello stack implica che verrà automaticamente liberato quando lo stack si svolge.

Nota rapida: come menzionato nella pagina man di Mac OS X per alloca (3), "La funzione alloca () dipende dalla macchina e dal compilatore; il suo uso è sconsigliato." Solo così lo sai.


4
Inoltre, l'ambito di alloca () è l'intera funzione, non solo il blocco di codice contenente la variabile. Quindi usandolo all'interno di un loop aumenterà continuamente lo stack. Un VLA non presenta questo problema.
sashoalm,

3
Tuttavia, i VLA che hanno l'ambito del blocco che lo racchiude indicano che sono significativamente meno utili di alloca () con l'ambito dell'intera funzione. Considera: if (!p) { p = alloca(strlen(foo)+1); strcpy(p, foo); } questo non può essere fatto con VLA, proprio a causa del loro ambito di blocco.
MadScientist,

1
Ciò non risponde ai PO perché la domanda. Inoltre, questa è una Csoluzione simile e non proprio C++-ish.
Adrian W,

13

Nel mio lavoro, mi sono reso conto che ogni volta che volevo qualcosa come array automatici di lunghezza variabile o alloca (), non mi importava davvero che la memoria fosse fisicamente situata nello stack della CPU, solo che proveniva da un qualche allocatore di stack che non ha comportato viaggi lenti nell'heap generale. Quindi ho un oggetto per thread che possiede un po 'di memoria da cui può spingere / pop buffer di dimensioni variabili. Su alcune piattaforme, permetto che questo cresca tramite mmu. Altre piattaforme hanno una dimensione fissa (di solito accompagnata da uno stack di CPU di dimensioni fisse, anche perché non mmu). Una piattaforma con cui lavoro (una console di gioco portatile) ha comunque un piccolo stack di CPU prezioso perché risiede in una memoria scarsa e veloce.

Non sto dicendo che non è mai necessario spingere buffer di dimensioni variabili nello stack della cpu. Onestamente sono stato sorpreso quando ho scoperto che questo non era standard, in quanto sembra certamente che il concetto si adatti abbastanza bene alla lingua. Per me, tuttavia, i requisiti "dimensione variabile" e "devono trovarsi fisicamente nello stack della cpu" non sono mai stati messi insieme. Si trattava di velocità, quindi ho creato il mio tipo di "stack parallelo per buffer di dati".


12

Esistono situazioni in cui l'allocazione della memoria dell'heap è molto costosa rispetto alle operazioni eseguite. Un esempio è la matematica a matrice. Se lavori con matrici di dimensioni ridotte, inserisci da 5 a 10 elementi e fai molte aritmetiche il sovraccarico malloc sarà davvero significativo. Allo stesso tempo, rendere le dimensioni una costante di tempo di compilazione sembra molto dispendioso e poco flessibile.

Penso che il C ++ sia così insicuro in sé che l'argomento di "provare a non aggiungere più funzionalità non sicure" non è molto forte. D'altra parte, poiché C ++ è probabilmente il linguaggio di programmazione più efficiente in termini di runtime che lo rende sempre più utile, sono sempre utili: le persone che scrivono programmi critici per le prestazioni useranno in larga misura C ++ e hanno bisogno di quante più prestazioni possibili. Spostare le cose dall'heap allo stack è una di queste possibilità. Ridurre il numero di blocchi heap è un altro. Consentire VLA come membri oggetto sarebbe un modo per raggiungere questo obiettivo. Sto lavorando a un suggerimento del genere. È un po 'complicato da implementare, è vero, ma sembra abbastanza fattibile.


12

Sembra che sarà disponibile in C ++ 14:

https://en.wikipedia.org/wiki/C%2B%2B14#Runtime-sized_one_dimensional_arrays

Aggiornamento: non è diventato C ++ 14.


interessante. Herb Sutter ne discute qui sotto Dynamic Arrays : isocpp.org/blog/2013/04/trip-report-iso-c-spring-2013-meeting (questo è il riferimento per le informazioni di Wikipedia)
Default

1
"Le matrici di dimensioni runtime e il dynarray sono stati spostati nella specifica tecnica delle estensioni di array", ha scritto il 78 gennaio 2016, 78.86.152.103 su Wikipedia: en.wikipedia.org/w/…
strager

10
Wikipedia non è un riferimento normativo :) Questa proposta non è diventata C ++ 14.
MM,

2
@ViktorSehr: qual è lo stato di questo wrt C ++ 17?
einpoklum,

@einpoklum Nessuna idea, usa boost :: container :: static_vector
Viktor Sehr

7

Questo è stato considerato per l'inclusione in C ++ / 1x, ma è stato abbandonato (questa è una correzione a ciò che ho detto prima).

Sarebbe comunque meno utile in C ++ poiché dobbiamo già std::vectorricoprire questo ruolo.


42
No, no, std :: vector non alloca i dati nello stack. :)
Kos,

7
"lo stack" è un dettaglio di implementazione; il compilatore può allocare memoria da qualsiasi luogo purché siano soddisfatte le garanzie sulla durata degli oggetti.
MM

1
@MM: Fiera abbastanza, ma in pratica non possiamo ancora utilizzare std::vectoral posto di, diciamo, alloca().
einpoklum,

@einpoklum in termini di ottenere l'output corretto per il tuo programma, puoi. Le prestazioni sono un problema di qualità dell'implementazione
MM

1
La qualità di implementazione di @MM non è portatile. e se non hai bisogno di prestazioni, non usi c ++ in primo luogo
amico

3

Usa std :: vector per questo. Per esempio:

std::vector<int> values;
values.resize(n);

La memoria verrà allocata sull'heap, ma ciò comporta solo un piccolo inconveniente delle prestazioni. Inoltre, è consigliabile non allocare grandi datablock nello stack, poiché è piuttosto limitato nelle dimensioni.


4
Un'applicazione importante per matrici di lunghezza variabile è la valutazione di polinomi di grado arbitrario. In tal caso, il tuo "piccolo inconveniente delle prestazioni" significa "il codice viene eseguito cinque volte più lentamente nei casi tipici". Non è piccolo.
AHelps

1
Perché non usi semplicemente std::vector<int> values(n);? Usando resizedopo la costruzione stai vietando i tipi non mobili.
LF

1

C99 consente VLA. E pone alcune restrizioni su come dichiarare VLA. Per i dettagli, fare riferimento a 6.7.5.2 della norma. C ++ non consente VLA. Ma g ++ lo consente.


Potete fornire un collegamento al paragrafo standard che state indicando?
Vincent,

0

Le matrici come questa fanno parte del C99, ma non fanno parte del C ++ standard. come altri hanno già detto, un vettore è sempre una soluzione molto migliore, motivo per cui gli array di dimensioni variabili non si trovano nello standatrd C ++ (o nello standard C ++ 0x proposto).

A proposito, per domande sul "perché" lo standard C ++ è così com'è, il newsgroup Usenet moderato comp.std.c ++ è il posto dove andare.


6
-1 Il vettore non è sempre migliore. Spesso sì. Sempre no. Se hai solo bisogno di un piccolo array, sei su una piattaforma in cui lo spazio dell'heap è lento e l'implementazione del vettore della tua libreria utilizza lo spazio dell'heap, questa funzione potrebbe benissimo essere migliore se esistesse.
Patrick M,

-1

Se si conosce il valore al momento della compilazione, è possibile effettuare le seguenti operazioni:

template <int X>
void foo(void)
{
   int values[X];

}

Modifica: è possibile creare un vettore che utilizza un allocatore di stack (alloca), poiché l'allocatore è un parametro modello.


18
Se conosci il valore in fase di compilazione, non hai bisogno di un modello. Usa X direttamente nella tua funzione non modello.
Rob Kennedy,

3
A volte il chiamante lo sa in fase di compilazione e il chiamante no, ecco a cosa servono i template. Naturalmente, nel caso generale, nessuno conosce X fino al runtime.
Qwertie,

Non è possibile utilizzare alloca in un allocatore STL - la memoria allocata da alloca verrà liberata quando il frame dello stack viene distrutto - ecco quando ritorna il metodo che dovrebbe allocare memoria.
Oliver,

-5

Ho una soluzione che ha funzionato davvero per me. Non volevo allocare memoria a causa della frammentazione di una routine che doveva essere eseguita molte volte. La risposta è estremamente pericolosa, quindi usala a tuo rischio e pericolo, ma sfrutta l'assemblaggio per riservare spazio sulla pila. Il mio esempio di seguito usa una matrice di caratteri (ovviamente altre variabili di dimensioni richiederebbero più memoria).

void varTest(int iSz)
{
    char *varArray;
    __asm {
        sub esp, iSz       // Create space on the stack for the variable array here
        mov varArray, esp  // save the end of it to our pointer
    }

    // Use the array called varArray here...  

    __asm {
        add esp, iSz       // Variable array is no longer accessible after this point
    } 
}

I pericoli qui sono molti, ma ne spiegherò alcuni: 1. Cambiare la dimensione della variabile a metà strada ucciderebbe la posizione dello stack 2. Superare i limiti dell'array distruggerebbe altre variabili e il possibile codice 3. Questo non funziona a 64 bit build ... necessita di un assembly diverso per quello (ma una macro potrebbe risolvere quel problema). 4. Specifico per il compilatore (potrebbe avere difficoltà a spostarsi tra i compilatori). Non ci ho provato, quindi davvero non lo so.


... e se vuoi lanciarlo da solo, potresti usare una classe RAII?
einpoklum,

Puoi semplicemente usare boost :: container :: static_vector tu.
Viktor Sehr,

Questo non ha equivalenti per altri compilatori che hanno un assembly più grezzo di MSVC. VC probabilmente capirà che è espcambiato e regolerà i suoi accessi allo stack, ma ad esempio in GCC lo spezzerai completamente, almeno se usi le ottimizzazioni e -fomit-frame-pointerin particolare.
Ruslan,
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.