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:
- Qua il programmatore intendeva creare 10 pulsanti per fare un tastierino numerico, ma si è confuso e ha sbagliato il valore di NUMPAD_DIGITS.
#define NUMPAD_DIGITS 9
void create_widgets() {
Widget *widgets[NUMPAD_DIGITS + 1];
for (int i = 0; i < NUMPAD_DIGITS; i++)
widgets[i] = new Widget();
}
- In questi esempi il programmatore sta creando array di dimensioni sbagliate. È interessante notare che se il tipo di
foo_no_size()
fosse diverso da char *, non avremmo una funzione equivalente astrlen()
a disposizione per sapere la lunghezza dell’array
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
}
- Qua il programmatore ha creato un array sovra dimensionato rispetto alle esigenze, sperando che sia sufficiente.
void read_data() {
char buffer[256];
// read from socket
}
- Qua il programmatore vorrebbe sapere se i due array hanno gli stessi elementi, ma il confronto è sempre falso.
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:
- garantisce la contiguità degli elementi in memoria, per cui è compatibile con gli array C;
- i suoi iteratori sono RandomAccess;
- l’inserimento in coda è garantito da standard essere O(1) ammortizzato;
- grazie al fatto che gli elementi sono contigui in memoria fornisce prestazioni praticamente imbattibili su tutte le CPU moderne;
- supporta gli operatori, come ad esempio =, <, >, ==;
- ha dimensione dinamica.
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:
std::array
conosce la sua dimensione;- ha le stesse performance del rispettivo array C;
- fornisce l’accesso checked ad un elemento tramite
at()
; - fornisce iteratori RandomAccess;
- supporta gli operatori, come ad esempio =, <, >, ==;
- ha la stessa API degli altri container della libreria standard, per cui può essere usato in codice generico.
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 QVector
. QVector
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::vector
. QVector
è 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.