HeisenBUG

di il
9 risposte

HeisenBUG

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?

9 Risposte

  • Re: HeisenBUG

    - utilizzare la funzione reserve() per prevenire la riallocazione.
    Può essere una soluzione parziale, in quanto se non riservi sufficente memoria, se superi la capacity del vector, una push_back successiva fa scattare la riallocazione.
    In un caso simile la soluzione che userei io è ottenere il primo iteratore a prescindere, e avanzare con la std::advance() alla posizione desiderata.
    
    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]);
    
        auto it  = v.begin();
        std::advance(it,3);
        cout << *it << endl;
    
    
    }
    
    (Ovviamente sto parlando in singlethreading).
  • Re: HeisenBUG

    Può essere una soluzione parziale, in quanto se non riservi sufficente memoria, se superi la capacity del vector, una push_back successiva fa scattare la riallocazione.
    Ovviamente con SOLUZIONE intendo che ho la certezza che la capacità del vector non sarà superata.
    In un caso simile la soluzione che userei io è ottenere il primo iteratore a prescindere, e avanzare con la std::advance() alla posizione desiderata.
    Scusa, ma non ho capito come intendi utilizzare l'iteratore.
  • Re: HeisenBUG

    Mi riferivo a questo:
    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.
    Solo gli iteratori possono essere invalidati se vengono aggiunti elementi.
    If the new size() is greater than capacity() then all iterators and references (including the past-the-end iterator) are invalidated. Otherwise only the past-the-end iterator is invalidated.
    http://en.cppreference.com/w/cpp/container/vector/push_back
    Pertanto se si usa un iteratore invalidato da una riallocazione, il riuso di tale iteratore comporta un crash.
    Quindi è preferibile ottenere sempre un iteratore valido e poi avanzarlo fino al punto desiderato (che comunque dev'essere interno all'intervallo di esistenza).
  • Re: HeisenBUG

    Continuo a non capire cosa intendi...

    Potresti modificare il suddetto programmino al fine di mostrarmi l'uso che vorresti fare degli iteratori?
  • Re: HeisenBUG

    
    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);
        auto it  = v.begin();
        foo(v[0]); // poniamo avvenga una riallocazione che invalidi gli iteratori.
        // cout << *it << endl; // crash
        
        it = v.begin();
        cout << *it << endl; // non più crash 
    
        foo(v[1]); // poniamo avvenga una seconda riallocazione che reinvalidi gli iteratori.
        // cout << *it << endl; // crash
        
        it = v.begin();
        cout << *it << endl; // non più crash 
    
    }
    
    Questo il succo del discorso.
  • Re: HeisenBUG

    Il problema è come capire quando avviene una riallocazione che invalida l'iteratore.
    Qualcosa del genere magari?
    #include <iostream>
    #include <vector>
    
    using namespace std;
    
    vector<int> v;
    
    void foo(const int &a)
    {
        auto it = v.begin();
        for(unsigned int i = 0; i < 5; ++i)
        {
            cout << *it << " ";
            if(v.size() == v.capacity())
            {
                v.push_back(i);
                it = v.begin();
            }
            else
            {
                v.push_back(i);
            }
        }
    }
    
    int main()
    {
        v.push_back(7);
        foo(v[0]);
    }
    Ma a questo punto penso sia meglio passare il parametro "a" per valore oppure utilizzare reserve(n) (ipotizzando di avere dati a sufficienza per fissare n).
  • Re: HeisenBUG

    Quel codice ha poco senso:
    1) perché it mostrerebbe sempre il primo elemento inserito (non lo avanzi mai)
    2) perché stai facendo degli inserimenti e a quel punto che size() superi o no capacity() è indifferente.
    3) perché ha poco senso portarsi dietro un iteratore potenzialmente invalido. Si sta prima a ricavarselo al momento.
    Un po' come portarsi dietro un puntatore che non si sa se sia valido o no.
    Quindi l'iteratore va ottenuto dopo eventuali operazioni di inserimento tramite push_back() non prima.
    Ma a questo punto penso sia meglio passare il parametro "a" per valore oppure utilizzare reserve(n) (ipotizzando di avere dati a sufficienza per fissare n).
    Ma di quale "a" stai parlando? Di questo?
    
    void foo(const int &a) // << questo a?
    
    o di un generico "a" il cui tipo è "vector<int>?"
    Perché tutto il discorso sugli iteratori e loro invalidazione nasce da questo commento:
    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.
    visto che nel codice che hai messo nel primo post non c'è traccia di niente che somigli vagamente a un vector.
  • Re: HeisenBUG

    Ma di quale "a" stai parlando? Di questo?
    void foo(const int &a) // << questo a?
    Ovvio... non ce ne sono altre!
    o di un generico "a" il cui tipo è "vector<int>?"
    Certo che no... se scrivo una cosa perchè dovrei intenderne un'altra?!
    Perché tutto il discorso sugli iteratori e loro invalidazione nasce da questo commento:
    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.
    visto che nel codice che hai messo nel primo post non c'è traccia di niente che somigli vagamente a un vector.
    Guarda mi sono riletto tutto il post iniziale e dal punto di vista logico non fa una piega, se c'è stato qualche fraintendimento probabilmente è perchè lo hai letto in modo superficiale.
    Inoltre se guardi bene il post iniziale ti accorgerai che nel codice relativo al programmino il vector c'è eccome... è dichiarato globalmente per simulare il vector utilizzato nel programma reale (dove il vector costituisce un membro statico della stessa classe a cui appartiene il metodo f_A()).
    Ho postato quel programmino per mostrare in modo semplice il problema che mi causava il crash... se lanciato infatti restituisce il seguente output
    7 x x x x
    (dove x rappresenta uno stesso generico numero diverso da 7) e non quello corretto che è
    7 7 7 7 7
    Nel post iniziale oltre a riportare il motivo del bug e alcune soluzioni, chiedevo anche se vi veniva in mente qualche altra soluzione.

    In effetti mi ero accorto che la discussione aveva preso una piega un po' strana... va beh non ci siamo capiti, ci può stare!
    Quel codice ha poco senso:
    1) perché it mostrerebbe sempre il primo elemento inserito (non lo avanzi mai)
    2) perché stai facendo degli inserimenti e a quel punto che size() superi o no capacity() è indifferente.
    3) perché ha poco senso portarsi dietro un iteratore potenzialmente invalido. Si sta prima a ricavarselo al momento.
    Un po' come portarsi dietro un puntatore che non si sa se sia valido o no.
    Quindi l'iteratore va ottenuto dopo eventuali operazioni di inserimento tramite push_back() non prima.
    Spero che adesso ti sia un po' più chiaro l'ultimo codice che ho postato e anche le mie perplessità riguardo all'uso degli iteratori (almeno per il modo in cui io ho interpretato il loro utilizzo).
  • Re: HeisenBUG

    Certo che no... se scrivo una cosa perchè dovrei intenderne un'altra?!
    In passato mi è capitato di partecipare a/e vedere discussioni dove il problema veniva "emulato" con codice diverso da quello che effettivamente dava il problema, quindi parto prevenuto sul codice "emulato".
    Inoltre se guardi bene il post iniziale ti accorgerai che nel codice relativo al programmino il vector c'è eccome... è dichiarato globalmente per simulare il vector utilizzato nel programma reale (dove il vector costituisce un membro statico della stessa classe a cui appartiene il metodo f_A()).
    In realtà mi riferivo al codice interno di f_B_vecchia() e f_B_nuova() dove avevi lamentato il problema, non al resto.

    Dal primo post:
    - 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);
    Passare un const int& è in pratica inutile, fare lo stesso con un oggetto grosso può fare la differenza.
    E io avevo inteso che tu indendessi passare l'intero oggetto di class_name per copia (a prescindere che il vector interno sia statico).
    Nel post iniziale oltre a riportare il motivo del bug e alcune soluzioni, chiedevo anche se vi veniva in mente qualche altra soluzione.
    E a me sono venuti in mente gli iteratori.
    va beh non ci siamo capiti, ci può stare!
    Ci può stare
Devi accedere o registrarti per scrivere nel forum
9 risposte