mercoledì 13 luglio 2011

HowTo: svuotare correttamente un STL Container (C++)

La libreria standard del C++ offre numerosi container, oggetti creati per contenere e organizzare altri oggetti. Il più famoso ed utilizzato è probabilmente vector. Molte documentazioni utilizzano però una frase sibillina nello spiegare la funzione del metodo clear() dei container:

il metodo clear elimina tutti gli oggetti contenuti, chiama i rispettivi distruttori, e pone a 0 la size del contenitore.

Una tale definizione sembra mettere al riparo da ogni problema il programmatore che decida (saggiamente) di usare un vector (o una list, un map, o queue) al posto di un comune array o di un oggetto custom. E invece non è così.

Facciamo qui un paio di esempi utilizzando due classi: Enemy e Level. Level conterrà una collezione di oggetti Enemy (anzi, di puntatori ad oggetti di tipo Enemy). Scopo dei due esempi è mostrare come svuotare correttamente tale collezione.

#include <iostream>
#include <vector>

//---------------------------------------------------------------
class Enemy
{
public:
   Enemy();
   ~Enemy();
};

Enemy::Enemy()
{
   std::cout << "Eseguo costruttore Enemy" << std::endl; 
}  

Enemy::~Enemy() 
{     
   std::cout << "Eseguo distruttore Enemy" << std::endl; 
}  

//---------------------------------------------------------------

class Level 
{ 
public:     
   Level();     
   ~Level();  
private:     
   std::vector<Enemy*> m_Enemies;
};

Level::Level()
{
   // creo e inserisco tre elementi Enemy nel vector     
   std::cout << "Eseguo costruttore Level" << std::endl;     

   m_Enemies.push_back(new Enemy());     
   m_Enemies.push_back(new Enemy());     
   m_Enemies.push_back(new Enemy()); 
}  

Level::~Level() 
{     
   // ripulisco il vector
   std::cout << "Eseguo distruttore Level" << std::endl;           
   m_Enemies.clear(); 
}  

//-------------------------------------------------------------- 

int main() 
{     
   Level* lev = new Level();      
   delete lev;      
   return 0; 
}  

Evidenziamo la classe Level, in particolare il suo distruttore. Notiamo che m_Enemies viene pulito tramite il metodo clear(). Sembra logico. Quel che ci attende però dopo l’esecuzione è questo output:



Mancano le chiamate al distruttore di Enemy, il che significa che clear() non ha correttamente liberato la memoria. La documentazione non è sbagliata, solo poco chiara. Se avessimo riempito il nostro vector con degli oggetti concreti anzichè con dei puntatori, il distruttore sarebbe stato chiamato correttamente. Ma, come nella maggior dei casi reali, abbiamo usato puntatori, e il programma non sa quale distruttore andare a pescare per distruggere l’oggetto puntato. Dobbiamo andargli un po’ incontro noi e modificare il distruttore di Level così:

Level::~Level() 
{     
   // ripulisco il vector     
   std::cout << "Eseguo distruttore Level" << std::endl;   
   
   while(!m_Enemies.empty())
   {
      Enemy* obj = m_Enemies.front();
      delete obj;
      m_Enemies.erase(m_Enemies.begin());
   }
}

E’ un po’ più lungo ma l’effetto è garantito. Quel che facciamo è ciclare sul vector finchè non risulta vuoto e ogni volta prendiamo l’elemento in testa. Chiamiamo esplicitamente il distruttore dell’oggetto con la delete ed eliminiamo il puntatore dal vector tramite erase, che diminuisce anche la size, impedendoci di entrare in loop con la guardia del while.

L’esecuzione darà ora questo risultato:



Buon coding!

4 commenti:

  1. Col vettore e' meglio usare pop_back() in modo da evitare che ad ogni erase() si debba ricompattare l'array.
    Probabilmente nella maggior parte dei casi non fa una differenza percepibile, ma e' meglio prendere l'abitudine 8)

    RispondiElimina
  2. Giusto, in effetti erase() per i vector ha complessità lineare, mentre pop_back() costante. Quindi meglio usare pop_back() al posto della erase() accoppiata alla begin() e, contestualmente, back() al posto di front

    RispondiElimina
  3. Questo commento è stato eliminato dall'autore.

    RispondiElimina
  4. Ringrazio Vuco per questa guida, che mi ha aiutato a risolvere un problema di memory leak per una lista di puntatori a puntatori di questo tipo:

    std::list[sf::Sprite**] tiles_onscreen;

    che con erase() proprio non si puliva!

    RispondiElimina