Volevo condividere con voi questo bug che nella giornata di ieri mi ha fatto compagnia per un bel po'.
A causa dell'elevato numero di righe di codice e della complessità dal punto di vista logico degli algoritmi (sto implementando strategie avanzate per la risoluzione dei sudoku), l'identificazione del bug, che portava al crash del programma, non è stata affatto facile.
Disattivando righe di codice e mettendo dei cout qua e là sono riuscito ad individuare la parte di codice problematica, si trattava di una funzione ricorsiva di questo tipo:
void f_A(const class_name &object_name)
Ho provato a ripercorrere più volte la logica della funzione alla ricerca di qualche errore, ma niente. Per cercare di capire qualcosa in più ho inserito un cout per stampare una variabile cont che contava il numero di ricorsioni, ed ecco che lanciando il programma il bug scompariva!
Inizialmente sono rimasto stupito perchè non mi era mai capitata qualcosa del genere, poi ragionando ho ipotizzato che le operazioni effettuate sulla variabile cont modificassero in qualche modo lo stato della memoria facendo scomparire il bug. Cercando su internet ho letto che problemi di questo tipo vengono chiamati, sulla scia del paradosso quantistico, heisenbug, in virtù del fatto che sembrano scomparire nel momento in cui vengono ricercati.
Visto che il debug andava a rilento, ho deciso di tagliare la testa al toro e sfruttare una vecchia versione funzionante dello stesso codice, sostituendo man mano le vecchie funzioni con quelle nuove. Ovviamente parto da f_A() e noto con stupore che il programma continua a funzionare! Proseguo allora con le altre funzioni, finchè aggiornando f_B() ecco che il bug si ripresenta. La funzione in questione ha la seconda struttura nelle due versioni:
int f_B_vecchia(const class_name &object_name)
{
int x;
if(object_name.member_name == 0)
{
...
}
else if(object_name.member_name == 1)
{
...
}
else
{
...
}
return x;
}
int f_B_nuova(const class_name &object_name)
{
int x;
if(object_name.member_name == 0)
{
...
}
else if(object_name.member_name == 1)
{
...
}
else if(object_name.member_name == 2)
{
...
}
return x;
}
Deduco quindi che il problema consiste nel fatto che member_name assuma dei valori diversi da quelli da me previsti (ossia 0, 1 e 2) e quindi la sostituzione di else con else if(member_name == 2) causa il crash del programma.
Decido allora di stampare i valori assunti da member_name in f_A() e come previsto leggo un valore anomalo (222). La cosa mi sembra cmq strana visto che l'oggetto passato a f_A() è valido e member_name non viene modificato in f_A() (ciò è anche assicurato dalla presenza dello specificatore const).
Decido quindi di fare un tentativo passando ad f_A() il parametro non per riferimento, ma per valore... ed ecco che il codice funziona!
Vado a controllare la chiamata di f_A() e noto che il parametro passato è un elemento di v (un vector<class_name>, membro statico di un'altra classe)... ed ecco che scatta la molla. In pratica poichè in f_A() possono potenzialmente essere aggiunti altri elementi a v, se la capacità di v viene superata il vettore viene riallocato con la conseguenza che i vecchi riferimenti puntano ad arie di memoria libere che possono quindi essere riscritte.
Il seguente programmino mostra in modo semplice una situazione simile:
#include <iostream>
#include <vector>
using namespace std;
vector<int> v;
void foo(const int &a)
{
for(unsigned int i = 0; i < 5; ++i)
{
cout << a << endl;
v.push_back(i);
}
}
int main()
{
v.push_back(7);
foo(v[0]);
}
Le soluzioni che mi vengono in mente sono essenzialmente due:
- passare il parametro per copia e non per riferimento. In f_A() avevo utilizzato il passaggio per riferimento perchè un oggetto di class_name è abbastanza grande (nel senso che non è certo un int);
- utilizzare la funzione reserve() per prevenire la riallocazione.
Vi vengono in mente altre soluzioni per risolvere il problema?
Avete qualche consiglio di buona programmazione per evitare di trovarsi in situazioni del genere?