Spaß mit stl::shared_ptr auf ein c-array in C++ (Memory Tester)

Spaß mit stl::shared_ptr auf ein c-array in C++ (Memory Tester)

Ich hatte schon einmal das Problem, eine Reihe von Klassen in einen STL Container einfügen zu wollen, wobei die Klassen jeweils sehr große Datenmengen (ein Array) enthalten und ich dabei über die Kopiererei der STL Container gestolpert bin (die dann entsprechend lange dauert).

Das letzte Mal hatte ich dann noch selber einen Container gebastelt, diesmal wollte ich schön und sauber mit der STL (habe gerade keine Boost installiert) entwickeln.

Dabei bin ich über ein paar Steine gestolpert und habe verschiedene Varianten durchgespielt.

Das generelle Setup ist so, dass es einen std::vector gibt, der Klassen enthält (bzw. später shared_ptr auf Klassen (MemoryStressChunk)). In so einer Klasse ist dann ein wenig Funktionalität (in diesem Fall erzeugen und checken eines Speicherbereiches) sowie eine größere Datenmenge (der Speicherbereich) als Array.

Variante a) (MemTester)

läuft so, dass in dem std::vector einfach nur Klassen drinstehen. Erst später ist mir aufgefallen, dass der std::vector beim Entfernen von Elementen nicht den delete Operator aufruft, weshalb es hier noch zu Speicherlecks kommt. (Aus diesem Grund habe ich für die späteren Varianten in den std::vector std::shared_ptr eingefügt.)
Da die Kopien obeflächlich sind (also im Falle von Pointern wie beim Array nicht etwa der Inhalt des Arrays mitkopiert wird, sondern lediglich die Referenz auf das Array, so also, dass zwischenzeitlich 2 Pointer auf das gleiche Array zeigen) hatte ich Probleme, automatisch einen korrekten Zeitpunkt zum Abräumen des Arrays zu finden. Immerhin wird der Destruktor ja mehrfach aufgerufen, wenn beispielsweise Objekte im STL Container hin und hergeschoben oder eingefügt werden.
Daher habe ich vor dem Entfernen der Klasse aus dem Array jeweils eine Funktion aufgerufen, die das Array freigibt. Korrekter wäre es vermutlich, sich eine Referenz auf die Klasse zu holen, die Klasse aus dem Vektor zu entfernen, und anschließend die Klasse per delete zu löschen (und dementsprechend auch die Freigabe des Arrays in den Destruktor zu packen).

1
2
3
4
5
6
7
8
9
10
		// definition
		std::vector<memoryStressChunk> memVector[maxThreads];
 
		// ersellen
		MemoryStressChunk *chunk = new MemoryStressChunk(chunkSize);
		memVector[threadNum].push_back(*chunk);
 
		// abbaun
		memVector[threadNum][index].CleanBeforeDestruct();
		memVector[threadNum].erase(memVector[threadNum].begin() + index);

Variante b)

umgeht das Problem der Notwendigkeit, manuell abzuräumen, indem für das Array ein std::shared_ptr benutzt wird. Dieser kümmert sich automatisch darum, wie oft eine Referenz auf ein Objekt existiert, und sobald es keine Referenz mehr gibt wird das Objekt abgeräumt. Da std::shared_ptr von Hause aus Arrays nicht unterstützt, habe ich das Array einfach in eine Klasse getan, diese erzeugt im Konstruktor das Array und räumt es im Destruktor wieder ab.

1
2
3
4
5
6
7
8
		// definition
		std::vector<std::shared_ptr<memoryStressChunk> > memVector[maxThreads];
 
		// ersellen
		memVector[threadNum].push_back( std::shared_ptr<memoryStressChunk>(new MemoryStressChunk(chunkSize)) );
 
		// abbaun
		memVector[threadNum].erase(memVector[threadNum].begin() + index);

Die Wrapper-Klasse für das Array ist minimal aufgebaut. Konstruktor, Destruktor (wird über den shared_ptr aufgerufen), shallow Copy Konstruktor sowie noch der [] Operator, damit von der Syntax her nichts angepasst werden muß. Syntaxmäßig funktioniert die Klasse per [] genauso wie ein Array.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
DataArray::DataArray(int vals)
{
	data = new __int64[vals];
}
 
DataArray::~DataArray(void)
{
	delete [] data;
}
 
DataArray::DataArray(const DataArray& obj)
{
	this->data = obj.data;
}
 
__int64 DataArray::operator[](const size_t i) const
{
	return data[i];
}
 
__int64& DataArray::operator[](const size_t i)
{
	return data[i];
}

Variante c)

hat recht wenig Spaß gemacht. Hier kommen die Tücken der Template-Syntax zu tragen. Wenn man nicht genau weiß, wann man [] oder () benutzt, einen * mehr oder weniger nimmt oder ob der Compiler evtl. meckert, wenn ein & fehlt ist man hier doch etwas länger unterwegs.
Die Idee ist ganz einfach: die Klasse aus der Variante b) wird wieder rausgeschmissen, statt dessen wird der shared_ptr einfach so aufgebohrt, dass er auch Arrays unterstützt. Das geht, indem ein eigener Destruktor mitgegeben wird, der delete [] statt delete aufruft (standardmäßig ruft shared_ptr nämlich delete auf). So einfach ist das dann auch. Bis auf die Syntax.

1
2
3
4
5
6
7
8
		// definition
		std::vector<std::shared_ptr<memoryStressChunk> > memVector[maxThreads];
 
		// ersellen
		memVector[threadNum].push_back( std::shared_ptr<memoryStressChunk>(new MemoryStressChunk(chunkSize)) );
 
		// abbaun
		memVector[threadNum].erase(memVector[threadNum].begin() + index);

Bis hierhin gibt es also noch nicht viel Neues.

Trickreicher wird es in der Klasse MemoryStressChunk.

Headerdatei MemoryStressChunk.h Auszug:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
template< typename T >
  struct
array_deleter
  {
      void
    operator ()( T const * p)
      { delete[] p; }
  };
 
class MemoryStressChunk
{
private:
	int chunkSizeInValues;
	//std::shared_ptr< __int64* > data;
	std::shared_ptr< __int64 > data;
	__int64 seed;
	(...)

Codedatei MemoryStressChunk.cpp Auszug:

1
2
3
4
5
6
7
8
9
10
11
12
	// Erzeugen des Arrays
	data.reset(
		new __int64[chunkSizeInValues],
		array_deleter< __int64 >() );
 
	// Benutzen des Arrays
	__int64 *d = data.get();
	for (int i = 0; i < chunkSizeInValues; ++i)
		d[i] = seed + i;
 
	// Abräumen des Arrays
	// automatisch :)

Wer mit dem Code rumspielen möchte oder auch nur mal seinen Speicher testen möchte findet hier die Visual Studio 2010 Projekte für alle 3 Varianten .

Schreibe einen Kommentar

Deine E-Mail-Adresse wird nicht veröffentlicht. Erforderliche Felder sind mit * markiert.