Reto de la semana: containers de la STL

El tema es sencillo (o lo parece). Cuando utilizamos un contenedor de la STL (vector por ejemplo) ¿qué pasa con el contenido cuando se destruye el contenedor?

Sirva este código para ilustrar la cuestión:

#include <vector>
#include <string>
 
using namespace std;
 
int main() {
 
  vector<string> v1;
  v1.push_back("hello");
  v1.push_back("world");
 
  vector<string>* v2 = new vector<string>(v1.begin(), v1.end());
 
  // many function calls…
 
  delete v2;
 
  // many function calls…
 
  return 0;
}

Al ejecutarse el delete v2, ¿se destruyen las cadenas que contiene? ¿Y las de v1? ¿cómo funciona esto? ¿pasa con todos los containers? ¿se puede cambiar ese comportamiento (sea el que sea)?

El reto consiste en:

Escribir un programa lo más sencillo posible (pero sin ofuscación) que demuestre inequívocamente qué le ocurre al contenido del container, y responda (en lo posible) esas preguntas.

Abstenerse Paco Moya por el momento, por favor Smiling

Comentarios

Opciones de visualización de comentarios

Seleccione la forma que prefiera para mostrar los comentarios y haga clic en «Guardar las opciones» para activar los cambios.
Imagen de cleto

No tengo una buena explicación

a la salida del siguiente fragmento:

#include <vector>
#include <string>
#include <iostream>
 
using namespace std;
 
class ejemplo
{
public:
  char _a;
  ejemplo ( char a ): _a(a) {
    cout << "constructor " << _a << endl;
  };
 
  ~ejemplo () {	
    cout << "destructor " << _a << endl;
  };
};
 
int main() 
{
   vector<ejemplo>* v = new vector<ejemplo> ();
 
 
   ejemplo e1('1');
   ejemplo e2('2');
   ejemplo e3('3');
 
   v->push_back(e1);
   cout << "Obj 1 added" << endl;
   v->push_back(e2);
   cout << "Obj 2 added" << endl;
   v->push_back(e3);
   cout << "Obj 3 added" << endl;
 
   delete v;
 
   cout << "End" << endl;
 
   return 0;
}

La salida es la siguiente:

constructor 1
constructor 2
constructor 3
Obj 1 added
destructor 1
Obj 2 added
destructor 1
destructor 2
Obj 3 added
destructor 1
destructor 2
destructor 3
End
destructor 3
destructor 2
destructor 1

Las tres últimas llamadas creo que son las que se producen por el destructor de ámbito, lo cual me inclina a pensar que las instancias NO se destruyen al realizar el delete sobre el vector.

Imagen de brue

cuando se meten en el vector....

.... se meten como una copia usando el default copy constructor, y parecen los mismos objetos.

Si cambiamos el constructor de copia por uno que no sea el de por defecto:

#include <vector>
#include <string>
#include <iostream>
 
using namespace std;
 
class ejemplo
{
public:
  char _a;
  ejemplo ( char a ): _a(a) {
    c = "Original";
    cout << "constructor " << _a << endl;
  }
 
  ejemplo (const ejemplo&){
    c = "Constructror de copia";
  }
 
  ~ejemplo () {	
    cout << "destructor ::  " << _a << " " << c  << endl;
  }
 
private:
  string c;
};
 
int main() 
{
   vector<ejemplo>* v = new vector<ejemplo> ();
 
   ejemplo e1('1');
   ejemplo e2('2');
   ejemplo e3('3');
 
   v->push_back(e1);
   cout << "Obj 1 added" << endl;
   v->push_back(e2);
   cout << "Obj 2 added" << endl;
   v->push_back(e3);
   cout << "Obj 3 added" << endl;
 
 
   delete v;
 
 
   cout << "End" << endl;
 
   return 0;
}

El "problema" ahora es explicar esta salida:

brue@doppler[20:26:50]~$ ./reto 
constructor 1
constructor 2
constructor 3
Obj 1 added
destructor ::   Constructror de copia
Obj 2 added
destructor ::   Constructror de copia
destructor ::   Constructror de copia
Obj 3 added
destructor ::   Constructror de copia
destructor ::   Constructror de copia
destructor ::   Constructror de copia
End
destructor ::  3 Original
destructor ::  2 Original
destructor ::  1 Original

brue

Imagen de david.villa

Copy constructor que te crió

El constructor de copia está claro que es determinante en el tema. Mirad qué curioso si le seguimos la pista a las copias:

#include <iostream>
#include <vector>
#include <string>
 
using namespace std;
 
 
class A {
  string val;
 
public:
  A() {
    cout << "creado: <vacío>" << endl;
  }
 
  A(string v) : val(v) {
    cout << "creado: "<< v << endl;
  }
 
  A(const A& other) {
    string newval = other.val + "-copy";
    cout << "copiado: " << other.val << " -> " << newval << endl;
    val = newval;
  }
 
  A& operator=(const A& other) {
    cout << "asignado: " << val << " = " << other.val << endl;
    val = other.val;
  }
 
  ~A() {
    cout << "destruido: " << val << endl;
  }
};
 
int main() {
 
  vector<A> v1;
  v1.push_back(A("x"));
  v1.push_back(A("y"));
 
  vector<A>* v2 = new vector<A>(v1.begin(), v1.end());
 
  // many function calls...
 
  delete v2;
 
  return 0;
}

Y la salida:

creado: x
copiado: x -> x-copy
destruido: x
creado: y
copiado: y -> y-copy
copiado: x-copy -> x-copy-copy
destruido: x-copy
destruido: y
copiado: x-copy-copy -> x-copy-copy-copy
copiado: y-copy -> y-copy-copy
destruido: x-copy-copy-copy
destruido: y-copy-copy
destruido: x-copy-copy
destruido: y-copy

No soy portavoz de ningún colectivo, grupo o facción. Mi opinión es personal e intransferible.

Imagen de brue

y ...

... la salida de esto?

#include <vector>
#include <string>
#include <iostream>
 
using namespace std;
 
class ejemplo
{
public:
  char _a;
  ejemplo ( char a ): _a(a) {
    c = "Original";
    cout << "constructor " << this << " " <<  _a << endl;
  }
 
  ejemplo (const ejemplo&){
    cout << "copyc " << this << endl;
    c = " Constructror de copia";
  }
 
  ~ejemplo () {
    cout << "destructor ::  " << this << " " << c  << endl;
  }
 
private:
  string c;
};
 
int main() 
{
   vector<ejemplo>* v = new vector<ejemplo> ();
 
   ejemplo e1('1');
   ejemplo e2('2');
   ejemplo e3('3');
 
   v->push_back(e1);
   cout << "Obj 1 added" << endl;
   v->push_back(e2);
   cout << "Obj 2 added" << endl;
   v->push_back(e3);
   cout << "Obj 3 added" << endl;
 
 
   delete v;
 
 
   cout << "End" << endl;
 
   return 0;
}

brue@doppler[20:48:35]~$ ./reto 
constructor 0x7fff205254c0 1
constructor 0x7fff205254b0 2
constructor 0x7fff205254a0 3
copyc 0x1d520c0
Obj 1 added
copyc 0x1d52130
copyc 0x1d52120
destructor ::  0x1d520c0  Constructror de copia
Obj 2 added
copyc 0x1d521f0
copyc 0x1d521d0
copyc 0x1d521e0
destructor ::  0x1d52120  Constructror de copia
destructor ::  0x1d52130  Constructror de copia
Obj 3 added
destructor ::  0x1d521d0  Constructror de copia
destructor ::  0x1d521e0  Constructror de copia
destructor ::  0x1d521f0  Constructror de copia
End
destructor ::  0x7fff205254a0 Original
destructor ::  0x7fff205254b0 Original
destructor ::  0x7fff205254c0 Original

Diferentes instancias ... a ver que quizá no he dormido mucho y no querría liaros más Smiling

brue

Imagen de brue

me respondo ...

... si no reservas de primeras, parece que en esta implementación de STL (GNU) el tamaño inicial por defecto es 1 y hay que hacer realloc en cada inserción. Si reservamos 3 posiciones, esto ya no pasa:

#include <vector>
#include <string>
#include <iostream>
 
using namespace std;
 
class ejemplo
{
public:
  char _a;
  ejemplo ( char a ): _a(a) {
    c = "Original";
    cout << "constructor " << this << " " <<  _a << endl;
  }
 
  ejemplo (const ejemplo&){
    cout << "copyc " << this << endl;
    c = " Constructror de copia";
  }
 
  ~ejemplo () {
    cout << "destructor ::  " << this << " " << c  << endl;
  }
 
private:
  string c;
};
 
int main() 
{
   vector<ejemplo>* v = new vector<ejemplo> ();
 
   v->reserve(3); // <-----------------
 
   ejemplo e1('1');
   ejemplo e2('2');
   ejemplo e3('3');
 
   v->push_back(e1);
   cout << "Obj 1 added" << endl;
   v->push_back(e2);
   cout << "Obj 2 added" << endl;
   v->push_back(e3);
   cout << "Obj 3 added" << endl;
 
 
   delete v;
 
 
   cout << "End" << endl;
 
   return 0;
}

brue

Imagen de brue

al contenido no le pasa nada...

... se queda ahí. Es obvio, imaginemos que tenemos dos vectores que comparten elementos. La responsabilidad de la destrucción del contenido es del programador, y no del contenedor.

Pero, y lanzo otra pregunta ... ¿y si en vez de clases propias son tipos como enteros?

Es decir vector<int> vEnteros;

brue

Imagen de david.villa

Eso es muy bonito de decir,

Eso es muy bonito de decir, pero el reto es escribir un programa que demuestre que es así.

No soy portavoz de ningún colectivo, grupo o facción. Mi opinión es personal e intransferible.

Imagen de brue

pues eso ...

... que la pista era falsa y que sí se libera/destruyen los elementos. Cuando metemos objetos como punteros a objetos son los punteros los que se liberan, y no los objetos. Veamos un pequeño ejemplo con enteros que apoye lo que digo, que si no sacan el palo Eye-wink

#include <iostream>
#include <vector>
 
using namespace std;
 
int main(){
  vector <int>* v1 = new vector<int>;
  v1->push_back(65535);
 
  int* p = &v1->at(0);
 
  cout << "Antes del delete:" << endl;
  cout << *p;
 
  delete v1;
 
  cout << "Despues del delete:" << endl;
  cout << *p;
 
  return 0;  }

brue@doped-2[14:32:01]~$ ./a.out 
Antes del delete:
65535
Despues del delete:
10

brue

Imagen de david.villa

Para que conste: tú opinas

Para que conste: tú opinas que al destruir el contenedor se destruyen los objetos que contiene. ¿Alguien opina algo distinto?

No soy portavoz de ningún colectivo, grupo o facción. Mi opinión es personal e intransferible.

Imagen de brue

era ...

...una pista falsa ... esperando a que se animase la gente Eye-wink

Si nadie dice nada mando un trozo de código hoy mismo.

brue

Imagen de magmax

Nuevos retos!!

Hmmm... Curioso problemilla...

Luego lo miraré con más tiempo, pero de buenas a primeras, con el código que has puesto, se podría decir que se destruyen también o, al menos eso dice Valgrind:

#include <vector>
#include <string>
 
using namespace std;
 
int main() 
{
     vector<string> v1;
     v1.push_back("hello");
     v1.push_back("world");
 
     vector<string>* v2 = new vector<string>(v1.begin(), v1.end());
 
     delete v2;
 
     return 0;
}

Basta ejecutarlo con valgrind y nos dirá el resultado:

$ valgrind ./main
==10320== Memcheck, a memory error detector
==10320== Copyright (C) 2002-2009, and GNU GPL'd, by Julian Seward et al.
==10320== Using Valgrind-3.6.0.SVN-Debian and LibVEX; rerun with -h for copyright info
==10320== Command: ./main
==10320== 
==10320== 
==10320== HEAP SUMMARY:
==10320==     in use at exit: 0 bytes in 0 blocks
==10320==   total heap usage: 6 allocs, 6 frees, 68 bytes allocated
==10320== 
==10320== All heap blocks were freed -- no leaks are possible
==10320== 
==10320== For counts of detected and suppressed errors, rerun with: -v
==10320== ERROR SUMMARY: 0 errors from 0 contexts (suppressed: 19 from 8)

Miguel Ángel García
http://magmax.org

Imagen de magmax

Segunda opinión.

Hola de nuevo.

He seguido investigando el tema, y cambio mi opinión:

#include <vector>
#include <string>
#include <iostream>
 
using namespace std;
 
class ejemplo
{
public:
   ejemplo ( )
     {
        cout << "constructor" << endl;
     };
 
   ~ejemplo ()
     {	
        cout << "destructor" << endl;
     };
};
 
int main() 
{
   vector<ejemplo*>* v = new vector<ejemplo*> ();
 
     v->push_back ( new ejemplo () ) ;
     v->push_back ( new ejemplo () ) ;
     v->push_back ( new ejemplo () ) ;
 
     delete v;
 
     return 0;
}

Si ahora lo ejecutamos con valgrind, obtenemos:

$ valgrind ./main
==26179== Memcheck, a memory error detector
==26179== Copyright (C) 2002-2009, and GNU GPL'd, by Julian Seward et al.
==26179== Using Valgrind-3.6.0.SVN-Debian and LibVEX; rerun with -h for copyright info
==26179== Command: ./main
==26179== 
constructor
constructor
constructor
==26179== 
==26179== HEAP SUMMARY:
==26179==     in use at exit: 3 bytes in 3 blocks
==26179==   total heap usage: 7 allocs, 4 frees, 43 bytes allocated
==26179== 
==26179== LEAK SUMMARY:
==26179==    definitely lost: 3 bytes in 3 blocks
==26179==    indirectly lost: 0 bytes in 0 blocks
==26179==      possibly lost: 0 bytes in 0 blocks
==26179==    still reachable: 0 bytes in 0 blocks
==26179==         suppressed: 0 bytes in 0 blocks
==26179== Rerun with --leak-check=full to see details of leaked memory
==26179== 
==26179== For counts of detected and suppressed errors, rerun with: -v
==26179== ERROR SUMMARY: 0 errors from 0 contexts (suppressed: 19 from 8)

Si nos fijamos, hay 7 allocs y 4 frees. Es decir: nos ha faltado por liberar exactamente cada uno de los elementos. Además, una ejecución normal sólo mostrará los mensajes del constructor, no los del destructor, por lo que es de asumir que no se está llamando al destructor de los elementos del vector.

Una posible pregunta sería: ¿dónde están los 7 allocs? Pues yo supongo que son: la reserva del vector, los 3 objetos de tipo "ejemplo" y los 3 nodos del vector que albergarán los objetos de tipo "ejemplo".

Un saludo!!

Miguel Ángel García
http://magmax.org

Imagen de david.villa

Pero esto es distinto. En tu

Pero esto es distinto. En tu progrma, el vector contiene punteros, no instancias, entonces, aunque se copien, se copian punteros que apuntan a las instancias que *tú* creaste con new y por tanto eres tú el que debería destruirlos con delete. Resumiendo, que si se usa un vector de punteros no nos aclara nada.

No soy portavoz de ningún colectivo, grupo o facción. Mi opinión es personal e intransferible.

Imagen de brue

falta liberar ...

... cada elemnto.

Si antes de destruir el vector, usamos:

for (unsigned i=0; i < v->size();++i)
       delete v->at(i);

el resultado de valgrind será:

brue@doppler[18:16:27]~$ valgrind ./j
==4899== Memcheck, a memory error detector
==4899== Copyright (C) 2002-2009, and GNU GPL'd, by Julian Seward et al.
==4899== Using Valgrind-3.5.0-Debian and LibVEX; rerun with -h for copyright info
==4899== Command: ./j
==4899== 
constructor
constructor
constructor
destructor
destructor
destructor
==4899== 
==4899== HEAP SUMMARY:
==4899==     in use at exit: 0 bytes in 0 blocks
==4899==   total heap usage: 7 allocs, 7 frees, 83 bytes allocated
==4899== 
==4899== All heap blocks were freed -- no leaks are possible
==4899== 
==4899== For counts of detected and suppressed errors, rerun with: -v
==4899== ERROR SUMMARY: 0 errors from 0 contexts (suppressed: 4 from 4)

brue

Imagen de magmax

Evidentemente

Hombre... Era evidente.

Se trataba de comprobar si se libera la memoria o no, no de hacer un código perfecto...

Miguel Ángel García
http://magmax.org