Programmieren II: boost

Es handelt sich hierbei um eine relativ große Gruppe an C++-Entwicklern aus aller Welt. Ziel dieser Community ist es, qualitativ hochwertige Bibliotheken zu entwickeln und zu sammeln, die den C++ Standard ergänzen. Bibliotheken, die sich in der Praxis bewähren und in der Entwicklung von C++-Programmen eine große Bedeutung erlangen, besitzen gute Chancen, eines Tages in den C++ Standard aufgenommen zu werden.

Programmieren II: boost - Installation, erstes Programm

Die Installation erfolgt nach der Beschreibung in

Beispielprogramm:

#include < iostream >
#include < boost/tokenizer.hpp >
#include < string >

int main() {
  std::string str = ";;Hello|world||-foo--bar;yow;baz|";
  typedef boost::tokenizer< boost::char_separator< char > > tokenizer;
  boost::char_separator< char > sep("-;|");
  tokenizer tokens(str, sep);

  for (tokenizer::iterator tok_iter = tokens.begin();
       tok_iter != tokens.end(); ++tok_iter)
       std::cout << "[" << *tok_iter << "] ";
  std::cout << "\n";
  return EXIT_SUCCESS;
}

Programmieren II: boost - Installation, erstes Programm

Übersetzen und Programmlauf:

$ g++ -I ../../../../boost_1_39_0/ example.cpp -o example
$ example
[Hello] [world] [foo] [bar] [yow] [baz] 
$

 

Hier gibt es einen Überblick über die verfügbaren Bibliotheken. Ein Online-Buch (in deutsch) erklärt einige der boost-Bibliotheken.

 

Hörsaalübung

  1. Installieren Sie auf Ihrem Rechner boost.
  2. Verdeutlichen Sie sich die Bibliothek Boost.tokenizer.
  3. Versuchen Sie, das o.a.Beispielprogramm zum laufen zu bringen.
  4. Erklären Sie mir die verwendeten boost-Klassen und -Methoden.

Programmieren II: boost - bessere Zeiger

C++ hat keine eingebaute Automatik zur Freigabe von auf dem Heap allokierten Speicher. d.h. man muss mittels new konstruierten Objekte explizit durch Aufruf von delete auch wieder freigeben.

Bei einem linearen Programmablauf stellt dies kein Problem dar. In realen Programmen muss man aber jederzeit mit dem Auftreten von Fehlern und Ausnahmen (Exceptions) rechnen. Das ordnungsgemäße Aufräumen des Heaps in sämtlichen Fehlersituationen sicherzustellen, kann ein erheblicher Implementierungs- und Testaufwand bedeuten.

Beispiel (buffer1.cpp):

#include < exception >
#include < iostream >

class Buffer {
public:
    Buffer();
    ~Buffer();

    void transcode( int k);
    // ...
};

Buffer::Buffer() {
    std::cerr << "a new Buffer has been constructed\n";
}

Buffer::~Buffer() {
    std::cerr << "the Buffer was destroyed\n";
}

void Buffer::transcode( int k) {
    if (k < 0) throw std::exception();
    std::cerr << "Buffer transcoded (" << k << ")\n";
}


Die Methode transcode wird u.U. eine Ausnahme!

Programmieren II: boost - bessere Zeiger

using namespace std;
int main() {
    try {
        Buffer* b = new Buffer();
        // ...
        b -> transcode( -2);
        // ...
        delete b;
    }
    catch (const exception& e) {
        cerr << "Program exception\n";
        exit(1);
    }
}

 

Die Ausgabe des Programms:

$ buffer1
a new Buffer has been constructed
Program exception $

Der Destruktor vom Buffer b wird nie aufgerufen!

Der Grund dafür ist, dass die Funktion transcode eine Ausnahme wirft und somit die Programmausführung direkt im catch Block fortgesetzt wird. Alle im try Block nach transcode stehenden Anweisungen werden nicht mehr ausgeführt, so auch nicht die delete Anweisung. Alos wird auch der von b belegte Speicherplatz nicht freigegeben.

Programmieren II: boost - bessere Zeiger

auto_ptr der STL

Die STL stellt dazu das Template auto_ptr (automatic pointer) bereit. (buffer2.cpp)

#include < memory >
...
using namespace std;
int main() {
    try {
        auto_ptr< Buffer > b (new Buffer());
        // ...
        b -> transcode( -2);
        // ...
    }
    catch (const exception& e) {
        cerr << "Program exception\n";
        exit(1);
    }
}

auto_ptr ist im Header <memory> definiert und bekommt als Templateargument den Typ des zu verwaltenden Objektes. Ein reales auto_ptr Objekt wird dann mittels eines Konstuktors erzeugt, der als Parameter den Zeiger auf den zu verwaltenden Bereich des Heaps bekommt.

Die Ausgabe des Programms:

$ buffer2
a new Buffer has been constructed
the Buffer was destroyed
Program exception $

Programmieren II: boost - bessere Zeiger

auto_ptr bietet eine einfache und wirksame Hilfestellung bei der Verwaltung von Heapspeicher und damit der Vermeidung von Speicherlecks.

Zu beachten ist:

  1. Das Template auto_ptr darf nicht für die Verwaltung von dynamisch mittels new[] allokierten Arrays verwendet werden. Denn auto_ptr stellt sicher, dass beim Verlassen des Gültigkeitsbereiches der Variablen automatisch delete aufgerufen wird. Arrays erfordern für ein korrektes Zerstören jedoch delete[].
  2. Per auto_ptr erzeugte Objekte dürfen nicht in STL Containern, wie vector, stack oder map verwendet werden. Die STL fordert, dass in Containern steckende Objekte ohne Seiteneffekte kopierbar und zuweisbar sind.
  3. Es ist undefiniert, was passiert, wenn zwei auto_ptr dasselbe Objekt referenzieren.
  4. Beim Kopieren eines auto_ptr in einen anderen zeigt die Quelle hinterher auf nichts.

Hier schafft boost Abhilfe.

Hörsaalübung

Entwickeln Sie Beispielprogramme, die die o.a. Punkte 3 und 4 demonstrieren.

 

Programmieren II: boost - bessere Zeiger

shared_ptr von boost

shared_ptr aus der Boost.smart_ptr-Bibliothek enthält einen Referenzzähler und ermöglicht es so, dass mehr als ein shared_ptr auf dasselbe Objekt zeigt. Das Kopieren des Inhalts eines shared_ptr in einen anderen ist ebenfalls möglich, ohne dass die Quelle ihre Gültigkeit verliert. Beim Zerstören des letzten shared_ptr wird das referenzierte Objekt als letzte Aktion freigegeben.

Beispiel (sharedPtr.cpp)

Zur Demonstration dient die Klasse payload. Zu ihrer Unterscheidung erhält der Konstruktor einen Buchstaben als Argument, der sich über die öffentliche Funktion printObjectId() ausgeben lässt. Beim Aufruf des Destruktors meldet auch er sich mit der Objekt-ID.

class payload {
  private:
    string id_;
  public:
    payload(const string& id) {
     id_ = id;
    }
    ~payload(){
     cout << "Destructor of object" << id_ << endl;
    }
    void printObjectId(void){
     cout << "Called for object" << id_ << endl;
    }
};

 

Programmieren II: boost - bessere Zeiger

Zunächst legt das Beispiel in main mit new zwei Pointer auf payload-Objekte an, denen es die IDs A und B zuweist. Danach legt es drei shared_ptr-Objekte an - A und B zeigen jeweils auf die zugehörigen payload-Objekte, und C ist eine Kopie von A.

int main(int argc, char **argv) {
    payload *objectA = new payload("A");
    payload *objectB = new payload("B");
    shared_ptr< payload > A(objectA), B(objectB), C;
    cout << "Assigning A to C" << endl;
    C=A;
    cout << "Calling printObjectId() on A,B.C ..." << endl;
    A->printObjectId();         // Dereferenzierung mit -> Operator
    (*B).printObjectId();       // Dereferenzierung * Operator
    (C.get())->printObjectId(); // Extraktion des Pointers
    cout << "Resetting shared_ptr B ..." << endl;
    B.reset();
    cout << "Resetting shared_ptr A ..." << endl;
    A.reset();
    cout << "Now C is " << (C.unique()?"unique":"a copy")
         << " while A is " << (A?"not empty":"empty") << endl;
    cout << "Leaving the program ..." << endl;
}

Der Aufruf der printObjectId()-Funktion für die payload-Objekte kann auf drei Arten erfolgen: Wie bei einem normalen Pointer lassen sich shared_ptr-Objekte über den Pfeil- und den Stern-Operator dereferenzieren. Man erhält so Zugang zu den Methoden von payload. Alternativ kann man mit der get()-Funktion von shared_ptr direkt auf den gespeicherten payload-Zeiger zugreifen.

Zunächst setzt man mit shared_ptr<T>::reset() den shared:ptr B zurück. Da es den einzigen Pointer auf objectB enthält, ruft dies automatisch dessen Destruktor auf. Von objectA gibt es jedoch eine Kopie im shared_ptr C. Setzt man A entsprechend zurück, bleibt objectA intakt. Demgemäß darf an dieser Stelle keine Ausgabe vom Destruktor erfolgen. Mit unique() lässt sich überprüfen, ob C die letzte Kopie besitzt.

Programmieren II: boost - bessere Zeiger

Ausgabe:

$ sharedPtr 
Assigning A to C
Calling printObjectId() on A,B.C ...
Called for objectA
Called for objectB
Called for objectA
Resetting shared_ptr B ...
Destructor of objectB
Resetting shared_ptr A ...
Now C is unique while A is empty
Leaving the program ...
Destructor of objectA
$