Tre modi per evitare gli array in C++ moderno

L’uso di array in stile C è considerato debito tecnico in C++ moderno. Gli array C sono scomodi da usare e sono fonti di bug, ma ancora oggi vedo tanto codice nuovo che li usa. Seguendo questi consigli riuscirai a migliorare sensibilmente la qualità del tuo codice in pochi semplici passi.

Problemi con gli array

I problemi con gli array si possono dividere in due grandi categorie:

1) Accessi fuori dai limiti;
2) Sovra dimensionamento.

Il primo si ha quando il programmatore non calcola correttamente le dimensioni dell’array, il secondo si ha quando il programmatore non conosce a priori il numero di elementi da gestire e pertanto usa un numero “abbastanza” grande.

Andiamo a vedere alcuni esempi:

    #define NUMPAD_DIGITS 9
void create_widgets() {
   Widget *widgets[NUMPAD_DIGITS + 1];
   for (int i = 0; i < NUMPAD_DIGITS; i++)
       widgets[i] = new Widget();
}
void foo_no_size(const char *arr) {
   int a[sizeof(arr)]; // error: should be strlen(arr) + 1
}
 
void foo_size(const char *arr, int count) {
   int a[sizeof(arr)]; // error: should be count
}
void read_data() {
  char buffer[256];
  // read from socket
}
void main() {
   int arr[2] {1,2};
   int arr2[2] {1,2};
   if (arr == arr2) { // Error: never true
       cout << "Arrays are equal\n";
   }
}

Questi problemi derivano da un comportamento automatico degli array chiamato “decay” (deterioramento) a puntatore: un qualsiasi array può essere assegnato ad un puntatore dello stesso tipo, perdendo quindi la dimensione dell’array, senza che questo sia un errore di compilazione.

Un altro problema derivato dal “decay” a puntatore è la comparazione tra array. Tipicamente, quando confronto due array, voglio confrontare il valore degli elementi contenuti nell’array, ma il comportamento di default è il confronto tra puntatori.

Infine non posso usare l’assegnamento per fare una copia membro a membro tra array.

Tutte le alternative presentate in questo articolo non fanno “decay” a puntatore in automatico, mantenendo così le informazioni sulla dimensione, e offrono operatori per il confronto e l’assegnamento.

Possibili soluzioni

Vediamo come è possibile ovviare a questi problemi sia in STL che in Qt.

Usa std::vector

Parafrasando un noto detto, nessuno è mai stato licenziato per aver usato un vector. Le sue caratteristiche la rendono la struttura dati giusta per il 90% dei casi d’uso e per questo dovrebbe essere la tua scelta di default (come suggerito da Herb Sutter in “C++ Coding Standards: 101 Rules, Guidelines, And Best Practices”). Vediamone insieme alcune:

Vediamo come si possono riscrivere gli esempi presentati al paragrafo precedente.

#define NUMPAD_DIGITS 10
void create_widgets() {
  vector<Widget*> widgets;
  for (int i = 0; i < NUMPAD_DIGITS; i++)
      widgets.push_back(new Widget);
}
 
void foo_no_size(vector<char> v) {
   vector<int> a(v.size());
}

Usa std::array

Se non puoi usare allocazione dinamica, a partire dal C++ 11 la libreria standard fornisce un contenitore a dimensione fissa che conosce la sua dimensione: std::array.

I vantaggi di std::array rispetto ad un array C sono:

Questo contenitore è molto facile da usare, vediamo un esempio:

int main()
{
   std::array<int, 3> a2 = {1, 2, 3};
   std::array<std::string, 2> a3 = { std::string("a"), "b" };
   // container operations are supported
   std::sort(a1.begin(), a1.end());
 
   // ranged for loop is supported
   for(const auto& s: a3)
       std::cout << s << ' ';
   std::cout << '\n';
   std::array<int, 3> a4 = a1; // Ok
   //std::array<int, 4> a5 = a1; // Error, wrong number of elements
}

Possiamo riscrivere gli esempi presentati nell’introduzione in modo più idiomatico così:

#define NUMPAD_DIGITS 10
void create_widgets() {
   array<Widget*, NUMPAD_DIGITS> widgets;
   for (auto &w : widgets)
       w = new Widget;
}

Usa QVector

Se stai scrivendo un programma Qt, la struttura dati di default può essere a scelta std::vector oppure QVectorQVector fornisce tutte le garanzie di std::vector e in più garantisce la compatibilità a livello di API con il resto del programma Qt.

Se sei un utilizzatore di vecchia data di Qt, forse la tua scelta di default ricade su QList. Tuttavia, se parliamo di Qt5 e precedenti, questa scelta va riconsiderata perché ci sono numerose limitazioni che impattano sulle performance, sia in termini di velocità che di memoria occupata. Di fatto, in Qt5, QList andrebbe usata solo per interagire con le API di Qt che la richiedono.In Qt6 non esiste più alcuna distinzione tra QVector e QList ed entrambi garantiscono le stesse prestazioni di un std::vectorQVector è semplicemente un typedef di QList che serve per mantenere la compatibilità a livello di codice sorgente.

Personalmente, preferisco usare QVector o QList rispetto a std::vector perché trovo la API più comoda da usare.

Compatibilità con codice C

A questo punto spero di averti convinto che non esiste alcuna ragione per usare array C in C++. In alcuni casi però dovresti poter interagire con API C che leggono o scrivono su array C, ad esempio strncpy(char *dest, const char *src, size_t n).

Tutti i container che ho descritto hanno dei metodi di compatibilità con gli array C proprio per gestire casi come questo.

Vediamo degli esempi:

int main()
{
   constexpr size_t len = 14;
   const char s[len] = "Hello world!\n";
   array<char, len> a;
   strncpy(a.data(), s, a.size());
  
   vector<char> v(len); // allocate space for `len` elements
   strncpy(v.data(), s, v.size());
 
   QVector<char> v(len); // allocate space for `len` elements
   strncpy(v.data(), s, v.size());
}

Il metodo data() ritorna un puntatore al primo elemento della memoria e il metodo size() ritorna il numero di elementi disponibili.

Nota che ho usato un costruttore di vector che crea un certo numero di elementi prima di chiamare la funzione, altrimenti size() è 0.

Conclusioni

In C++ non esistono ragioni plausibili per usare array stile C. Le soluzioni che abbiamo visto in questo articolo sono tutte type e size safe, hanno funzionalità aggiuntive (come ad esempio la gestione automatica del resize oppure gli operatori di confronto) e sono compatibili con gli array C nei casi in cui ci sia da interagire con librerie C.

std::vector è il sostituto corretto per la maggior parte dei casi, QVector (o QList da Qt6) è preferibile per il codice che usa Qt , mentre std::array può essere usato in quei casi in cui un controllo granulare sulla memoria è fondamentale.