C++09 (Teil 2) - Ein Überblick: Die Standardbibliothek



  • C++09 (Teil 2) - Ein Überblick: Die Standardbibliothek

    Dies ist der zweite Teil der Serie über den wahrscheinlich 2009 erscheinenden neuen Standard für C++.

    Mit dem Technical Report (TR1) hat schon 2005 zuallererst die Standardbibliothek ein neues Gesicht bekommen, vor allem Boost diente dabei als hervorstechende Quelle der Inspiration. Der Technical Report 1 wird in C++09 vollständig enthalten sein, auch wenn teilweise Namen verändert und neue Funktionalitäten hinzugefügt werden.

    Damit wird die Standardbibliothek von C++09 jener Bereich sein, in dem die meisten Neuerungen stattfinden werden. Der Bereich im C++-Standard, der ihr gewidmet ist hat sich allein von der Seitenanzahl her jetzt schon seit C++03 beinahe verdoppelt (von knapp unter 400 Seiten auf über 700 Seiten im aktuellen Working-Draft).

    Der Hauptteil besteht dabei aus Erweiterungen aus dem TR1, ein weiterer großer Teil beschreibt die Unterstützung von paralleler Verarbeitung (Multithreading-Bibliothek und atomare Operationen). Ich möchte hier nicht allzu sehr ins Detail gehen, vor allem nicht Details erklären oder Schnittstellenspezifikationen aufschreiben, dafür gibt es ja den Standard selbst. In erster Linie möchte ich Beispiele geben, die zukünftige Möglichkeiten der Standardbibliothek aufzeigen.

    Hinweis: Ich verwende in den Code-Beispielen bewusst exemplarisch neue Sprachfeatures und damit die teilweise recht ungewohnte Syntax von C++09. Dazu gehört vor allem:

    • Die einheitliche Form der Initialisierung mit geschwungenen Klammern
    • Variadic Templates
    • Rvalue-Referenzen
    • Die neue Form der Funktionsdeklaration

    Für ein besseres Verständnis des Codes ist daher die Lektüre der entsprechenden Kapitel aus dem ersten Teil der Reihe (C++09 - Ein Überblick: Sprachfeatures) empfehlenswert.

    Inhalt

    • 1 Erweiterungen aus dem TR1
    • 1.1 Utilities
    • 1.2 Funktionsobjekte
    • 1.3 Neue Container
    • 1.4 Type-Traits
    • 1.5 Reguläre Ausdrücke
    • 1.6 Zufallszahlen
    • 2 Multithreading
    • 2.1 Threads
    • 2.2 Mutexe und Monitore
    • 3. Datum und Zeit
    • 4 System-Fehler
    • 5 Integration neuer Sprachfeatures
    • 5.1 Standard-Concepts
    • 5.2 Noch mehr: std::string
    • 6 Erweiterungen für den TR2 und danach
    • 6.1 Filesystem
    • 6.2 Weitere mögliche Neuerungen
    • 7 Ausblick
    • 8 Verweise

    1 Erweiterungen aus dem TR1

    Der Technical Report 1 ist ein Dokument, in dem verschiedene Erweiterungen der C++-Standardbibliothek vorgeschlagen werden. Der TR1 wurde 2005 veröffentlicht und viele der Funktionen werden von aktuellen Compilern (bzw. Standardbibliotheken) bereits unterstützt, teils auch bereits mit Hilfe der neuen Sprachfeatures.

    1.1 Utilities

    Der TR1 definiert einige nützliche Klassen, die das Leben in vielen Bereichen einfacher machen. Zu ihnen gehören

    • Reference Wrapper. Diese sind nützlich, um Referenzen an Funktionstemplates zu übergeben, die eigentlich Kopien ihrer Argumente anfertigen würden.
    • Smart Pointer. Zeiger, die sich selbst um die Freigabe des Speichers kümmern.
    • Tuple. Ein Container fixer Länge, der Daten verschiedenen Typs aufnehmen kann.

    Beispiel

    #include <functional> //Reference-Wrapper
    #include <memory>     //Smart-Pointer
    #include <tuple>      //Tuple
    
    #include <vector>
    using namespace std;
    
    template <class T>
    void fun (T t) 
    {
       t += 21;
    }
    
    int main ()
    {
       int i = 21;
       fun (ref(i)); //Einen Reference-Wrapper erstellen
       //i == 42
    
       auto p = make_shared<int>(42); //new int(42) für Smart-Pointer
       *p = 23;                       //p verhält sich wie ein Zeiger.
       //Nur: das delete geschieht automatisch.
    
       //Dynamischen std::vector mit Inhalt 1, 2, 3, 4 erzeugen:     
       auto vi = make_shared<vector<int>>({1,2,3,4}); 
    
       //Beliebig viele verschiedene Typen in einem Tupel speichern:
       auto foo = make_tuple(5, 4.2, 2.3);
       auto bar = make_tuple("std::string"s, 'c');
    
       //Zwei tuple-Instanzen vereinigen
       auto fubar = concatenate(foo, bar);
    
       //Werte auslesen
       auto first = get<1>(foo);  //decltype(first) -> int
       auto second = get<2>(foo); //decltype(second) -> double
    }
    
    //Funktion, die mehrere Rückgabewerte hat:
    auto fun () -> tuple<int, vector<int>, string>
    {
       return make_tuple(5, { 5,4,3,2,1 }, "foobar"s);
    }
    

    Erläuterung
    Wie der Header <functional> schon vermuten lässt, werden Reference-Wrapper hauptsächlich in Verbindung mit Funktionen höherer Ordnung verwendet. Ein weiteres Beispiel hierzu findet sich in 1.2.

    Smart-Pointer sind schon eine Weile im Umlauf. Der Standard von 2003 bot bereits einen: std::auto_ptr . Dieser hatte jedoch nicht immer die Semantik, die man erwartet hätte (Move-Semantik statt Kopiersemantik). Sein größter Nachteil war, dass er nicht in Standard-Containern verwendet werden konnte. std::auto_ptr ist mit C++09 deprecated und kann durch std::shared_ptr bzw. std::unique_ptr ersetzt werden, je nach Aufgabe.

    std::tuple ist dazu gedacht, std::pair zu ersetzen, denn es kann nicht nur zwei sondern beliebig viele Werte unterschiedlichen Typs aufnehmen. std::pair wird mit C++09 ebenfalls deprecated sein.

    Status
    Der TR1 ist Teil des aktuellen Working Drafts des kommenden C++-Standards.

    Unterstützung
    Der TR1 wird von allen größeren Standardbibliothek-Implementierungen unterstützt. Da der TR1 verbietet, dass seine Klassen und Funktionen direkt in den Standardheadern eingebaut werden, muss man diese zumeist explizit aktivieren (entweder über ein Makro oder durch einen anderen Include-Pfad, bspw. <tr1/functional> . Zu beachten ist auch, dass bestimmte Funktionen wie etwa make_shared nicht im TR1 enthalten ist, da es auf Rvalue-Referenzen basiert (siehe Teil 1 der Reihe). Zumindest die libstdc++ implementiert in Version 4.3 einige Funktionalität mithilfe der neuen Sprachfeatures (Dazu muss man beim Kompilieren -std=c++0x aktivieren). Allerdings gibt es keine vollständige Implementierung des TR1.

    Proposals
    N1403 - Tuple
    N1450 - Smart-Pointer
    N1453 - Reference-Wrapper
    N1836 - Der Draft des TR1

    1.2 Funktionsobjekte

    Bereits C++03 bot einige Funktionen höherer Ordnung an, das sind Funktionen, die andere Funktionen als Argument erwarten und mit ihnen umgehen. Die Verwendung von ihnen war jedoch oft nur eingeschränkt möglich. Mit std::function gibt es nun einen polymorphen Funktionswrapper, der auch Zeiger auf Memberfunktionen von Klassen beinhalten kann. Außerdem wurde die Beschränkung von std::bind1st und std::bind2nd , Argumente jeweils nur an den ersten oder zweiten Parameter binden zu können zu Gunsten von std::bind , das mit Funktionen mit unbestimmter Anzahl an Parametern arbeitet, aufgegeben.

    Beispiel

    #include <functional> //Binder, Funktionsobjekte
    
    #include <algorithm>
    using namespace std;
    
    //Addiert eine Zahl zu einer anderen:
    template <typename T>
    int accumulate (T value, T& acc)
    {
       return acc += value;
    }
    
    //Addiert zwei Zahlen und zählt mit, wie oft es aufgerufen wurde
    template <class T>
    struct Adder
    {
       int count;
       Adder () : count(0) {}
       T add (T a, T b) { ++count; return a + b; }
    };
    
    int main ()
    {
       vector<int> v1 { 1,2,3 };
    
       int value = 0;   
       for_each (v1.begin(), v1.end(), bind(accumulate<int>, placeholders::_1, ref(value)));
       //value == 1 + 2 + 3
    
       //Mache aus der Function "accumulate" eine neue Funktion, die eine Zahl inkrementiert:
       function<int(int&)> incrementor = bind(accumulate<int>, 1, placeholders::_1);
    
       incrementor(value);
       //value == 7
    
       //Auch Elementfunktion können verwendet werden:
    
       Adder<int> x;
       //Addiert 5 zu jedem Element
       transform (v1.begin(), v1.end(), v1.begin(), bind(&Adder<int>::add, &x, placeholders::_1, 5));
    
       //x.count == 3
       //v = { 6, 7, 8 }
    }
    

    Erläuterung
    std::bind ist dazu gedacht, die Standardfunktionen std::bind1st und std::bind2nd zu ersetzen. Um dies zu erreichen, werden "Platzhalter" eingeführt. Aus einer Funktion, die also eigentlich zwei Argumente benötigt, kann auf diese Art und Weise eine unäre Funktion gemacht werden (s.u.) - genauso können die Parameter einer Funktion mithilfe dieser Platzhalter quasi vertauscht werden. Die Platzhalter sind dabei durchnummeriert ( std::placeholders::_1 , std::placeholders::_2 , ...), wobei die Zahl den jeweiligen Parameter bezeichnet, den das so entstehende Funktionsobjekt übernimmt.

    void foo_binary (int, int);
    function<void (int)> foo_unary = bind (foo_binary, 42, placeholders::_1);
    //Platzhalter placeholders::_1 bedeutet also, dass das Funktionsobjekt
    //foo_unary genau ein Argument übernehmen kann. Dieses wurde mittels bind an
    //den zweiten Parameter der ursprünglichen Funktion, foo_binary, gebunden.
    //Das erste Argument, mit dem foo_binary aufgerufen wird, ist im Gegensatz
    //dazu kein Platzhalter, sondern schlicht und einfach "42".
    
    foo_unary (23);
    //gleichbedeutend mit:
    foo_binary (42, 23);
    
    function<void (int, int)> foo_reversed = bind (foo_binary, placeholders::_2, placeholders::_1);
    //Hier wird das erste Argument, mit dem das Funktionsobjekt aufgerufen wird,
    //(placeholders::_1) an den zweiten Parameter der ursprünglichen Funktion
    //gebunden; analog dazu wird das zweite Argument, mit dem das Funktionsobjekt
    //aufgerufen wird (placeholders::_1), an den ersten Parameter von foo_binary
    //gebunden.
    
    foo_reversed (1, 2);
    //gleichbedeutend mit:
    foo_binary (2, 1);
    

    Erläuterung
    Da C++09 jedoch direkt Closures (Lambda-Funktionen) unterstützt, werden viele Bereiche, in denen momentan boost.bind eingesetzt wird, durch diese übernommen. So lässt sich obiges Beispiel mit Closures auf folgende Art und Weise implementieren:

    #include <functional> //Binder, Funktionsobjekte, std::reference_closure
    #include <algorithm>
    using namespace std;
    
    int main ()
    {
       vector<int> v1 { 1,2,3 };
    
       int value = 0;   
       for_each (v1.begin(), v1.end(), [&value](int element) (value += element));
       //value == 1 + 2 + 3
    
       //Speichere ein (Reference-)Closure
       reference_closure accumulate = [](int value, int& acc) -> int { return acc += next; };
    
       //Mache aus dem Closure "accumulate" eine neue Funktion, die eine Zahl inkrementiert:
       function<int(int&)> incrementor = bind(accumulate, /* inkrementiere um */ 1, placeholders::_1);
    
       incrementor(value);
       //value == 7
    }
    

    Erläuterung
    Dieses Beispiel zeigt, dass sich mit Closures viele Gebiete, die derzeit von umständlich zu erstellenden Funktionsobjekten abgedeckt werden, einfacher implementieren lassen.

    Status
    siehe 1.1

    Unterstützung
    siehe 1.1
    Closures und damit auch std::reference_closure werden allerdings noch von keinem Compiler unterstützt.

    Proposals
    N1402 - Polymorphe Funktionsobjekt-Wrapper
    N1836 - Der Draft des TR1

    1.3 Neue Container

    TR1 definiert drei neue Container, die dieselbe Schnittstelle wie die anderen Standardcontainer anbieten. Es sind dies:

    • std::array für nicht-dynamische Arrays fixer Größe
    • std::unordered_set , die gehashte Version von std::set
    • std::unordered_map , die gehashte Version von std::map
    • C++09 enthält außerdem noch einen zusätzlichen Container, std::forward_list

    Beispiel

    #include <array>
    #include <unordered_map>
    #include <unordered_set>
    #include <forward_list>
    
    int main ()
    {
       std::array<int, 20> array;
       std::unordered_map<string, int> unordered_map;
       std::unordered_set<double> unordered_set;
    
       std::forward_list<int> li = { 1, 2, 3, 4 };
    }
    

    Erläuterung
    std::unordered_map und std::unordered_set lassen sich – vom Interface her – ebenso verwenden wie die Standardcontainer std::map und std::set . Statt einen Vergleich mit dem Kleiner-als-Operator durchzuführen (standardmäßiges Verhalten bei std::map und std::set ), wird hier eine Hash-Funktion verwendet. Um mit benutzerdefinierten Typen arbeiten zu können, muss daher eine Spezialisierung von std::hash (Header <hash> ) implementiert werden.
    std::forward_list ist eine einfach verkettete Liste, im Gegensatz zu std::list , die doppelt verkettet ist.

    Status
    siehe 1.1

    Unterstützung
    siehe 1.1

    Proposals
    N1456 - Hashtables
    N1548 - Array
    N1836 - Der Draft des TR1
    N2545 - Singly-linked list

    1.4 Type-Traits

    Type-Traits sind ein beliebtes Mittel der Template-Metaprogrammierung, das hilft, generischen Typen bestimmte Constraints zu setzen. Ob und wie weit sich dies durch den Einsatz von Konzepten (siehe hierzu Teil 1 der Reihe) ersetzen lässt, wird sich zeigen.

    Beispiel

    #include <type_traits>
    using namespace std;
    
    template <typename T, class Base, class Derived>
    struct Foo
    {
       static_assert (is_integral<T>::value, "T ist nicht integral!");
       static_assert (is_base_of<Base, Derived>::value, "Erwarte voneinander abgeleitete Typen!");
    };
    
    template <typename T>
    void foo (T const& t, typename enable_if<is_pod<T>>::type * = 0)
    {
    }
    
    struct Virtual
    {
       virtual ~Virtual () = default;
    };
    
    int main ()
    {
       //OK, 5 ist plain-old-data
       foo(5);
    
       //Ups, polymorphe Klasse ist kein POD - Kompilierfehler
       foo(Virtual());
    }
    

    Erläuterung
    Es stellt sich bei Type-Traits natürlich die Frage, inwieweit sie durch Konzepte (siehe Teil 1 der Reihe) abgelöst werden. Diese bieten ein eleganteres Interface, um Template-Parametern gewisse Constraints aufzuerlegen.

    Status
    siehe 1.1
    Im TR1 allerdings nicht enthalten sind std::enable_if , std::conditional und std::decay . Diese sind allerdings im Working-Draft des kommenden C++-Standards beinhaltet.

    Unterstützung
    Die im TR1 nicht enthaltenen Type-Traits befinden sich auch nicht im Namensbereich std::tr1 . Als Alternative lassen sich bisweilen die entsprechenden Type-Traits aus der Boost-Bibliothek verwenden. Für std::decay gibt es allerdings meines Wissens nach noch keine Implementierung.

    Proposals
    N1424 - Type Traits
    N1836 - Der Draft des TR1
    N2240 - std::enable_if und std::conditional
    N2244 - std::decay

    1.5 Reguläre Ausdrücke

    Reguläre Ausdrücke oder "regular expressions" (RegExp, RegEx) sind Zeichenketten, die eine Art Filterkriterium für Texte darstellen. So ist es möglich mit einem regulären Ausdruck einen Text zu beschreiben, der mit einem "A" beginnt, nur aus Buchstaben besteht und mit einem Punkt endet. Einsätze sind dabei vor allem komplizierte Suchausdrücke, aber auch Ersetzungen, indem man die zu suchenden Textteile als reguläre Ausdrücke definiert. Reguläre Ausdrücke sind in vielen Programmiersprachen gang und gäbe.

    Beispiel

    #include <regex>
    
    #include <iostream>
    #include <string>
    using namespace std;
    
    int main ()
    {   
       char const* text = "Ein langer Text\tmit Trennzeichen; Tja";
    
       smatch matches; //typedef für match_results<string::const_iterator>
    
       //Splitte Text nach Trennzeichen
       if (regex_search(text, matches, regex("[ ,.\\t\\n;:]"))
       {
          for (auto match: matches)
          //decltype(match) -> std::sub_match<std::string::iterator> bzw. std::ssub_match
          {
             cout << match << '\n';
          }
       }
    }
    

    Erläuterung
    Ein regulärer Ausdruck kann auf verschiedene Art und Weisen implementiert sein. Die Standardbibliothek liefert die Möglichkeit, zu testen, welche Art von regulären Ausdrücken sie unterstützt, dieses Feature wurde im Beispiel der Einfachheit halber jedoch nicht verwendet. Die Klasse std::regex bzw. std::basic_regex<> dient dafür, die Engine, die im Hintergrund Strings parst, zu abstrahieren. Mithilfe verschiedener Standardfunktionen wie std::regex_search oder std::regex_replace lässt sich so mit regulären Ausdrücken arbeiten.

    Ein eigener Container, der Matches für reguläre Expressions aufnehmen kann, ist dabei std::match_results , bzw. die für std::string explizit spezialisierte Version std::smatch . Da std::match_results einen Container darstellt, ist es möglich, mittels Iteratoren einzelne Matches ( std::sub_match bzw. für std::strings std::ssub_match ) auszulesen. Dieses ist im Prinzip einfach von std::pair<Iterator, Iterator> abgeleitet, besitzt aber einen Konvertierungsoperator nach std::string .

    Ein weiteres Feature von regulären Ausdrücken in C++09 ist, dass es auch mit den C++-Locales zusammenarbeitet. Eine eigene Regex-Traits-Klasse ( std::regex_traits ), die für einzelne reguläre Ausdrücke individualisiert werden kann, sorgt dabei für die Anbindung an bestimmte Locales – dabei ist anzumerken, dass eigene Spezialisierungen von std::regex_traits auch non-C++-Locales verwenden können. Regex-Traits erlaubt außerdem, die Syntax von regulären Ausdrücken den eigenen Gewohnheiten anzupassen.

    Alles in allem stellt die C++09-RegEx-Bibliothek ein mächtiges Werkzeug dar, um mit regulären Ausdrücken auch international arbeiten zu können.

    Status
    siehe 1.1

    Unterstützung
    Die C++09-RegEx-Bibliothek wird, obwohl sie bereits im TR1 enthalten war, meines Wissens noch von keinem Compiler unterstützt.

    Proposals
    N1429 - Reguläre Ausdrücke
    N1836 - Der Draft des TR1

    1.6 Zufallszahlen

    Der TR1 definiert ein mächtiges Interface, um Zufallszahlen zu erzeugen. Er definiert dabei sogenannte "Engines", die Zufallszahlen nach bewährten Algorithmen erzeugen und stellt diverse Verteilungen zur Verfügung, die mit diesen Engines arbeiten können.

    Beispiel

    #include <random>
    using namespace std;
    
    int main ()
    {
       //Zufallszahlen-Seeder: Erwartet in dem Fall Eingaben von std::cin
       seed_seq seeder {istream_iterator<int>(cin), istream_iterator<int>()};
    
       mt19937 mersenne_twister_engine {seeder};
       ranlux24 discard_block_engine {seeder};
       knuth_b shuffle_order_engine {seeder};
    
       bernoulli_distribution distribution_a {0.25};
       uniform_int_distribution<int> distribution_b {0, 99};
       weibull_distribution<double> distribution_c; 
    
       bool a = distribution_a (discard_block_engine);
       int b = distribution_b (shuffle_order_engine);
       double c = distribution_c (mersenne_twister_engine);
    }
    

    Status
    siehe 1.1

    Unterstützung
    Die tatsächlichen Bezeichnungen weichen zum Teil vom Vorschlag im TR1 ab. Außerdem wird meines Wissens nach die Zufallszahlen-Bibliothek von C++09 noch von keinem Compiler zu 100% unterstützt.

    Proposals
    N1836 - Der Draft des TR1
    N2111 - Die Zufallszahlen-Bibliothek

    2 Multithreading

    C++09 bietet den langersehnten Multithreading-Support. Dies geschieht einerseits über die Multithreading-Bibliothek, die Objekte für Threads, Mutexe und Locks und Monitore (Condition Variables) bereitstellt.

    2.1 Threads

    In Zeiten, in denen Prozessoren mehrere Kerne haben, ist es sinnvoll, Berechnungen auf diese zu verteilen. Auch Programme auf Single-Core-Prozessoren können durch den Einsatz von Threads verbessert werden, indem einzelne Komponenten des Programms voneinander entkoppelt jeweils den eigenen Anforderungen entsprechend Systemzeit verbrauchen können.

    Beispiel 1 - Neue Threads erstellen

    #include <thread>
    
    #include <functional>
    #include <algorithm>
    #include <iostream>
    #include <iterator>
    #include <vector>
    
    void bar ()
    {
    }
    
    struct MyThread
    {
       void start (int data)
       {
          //An den nächsten Thread weitergeben
          this_thread::yield();
       }
    };
    
    int main ()
    {
       //Starte neuen Thread mit Aufruf von bar();
       std::thread thread_a {bar};
    
       MyThread instance;
       //Starte neuen Thread mit Aufruf von instance.start(42);
       std::thread thread_b {std::mem_fun(&MyThread::start), &instance, 42};
    
       std::vector<int> data;
       //Kopiere in einem neuen Thread Daten von cin in den vector
       std::thread reader
       (
          [&]()
          (
             std::copy(std::istream_iterator<int>(std::cin), std::istream_iterator<int>(),
                       std::back_inserter(data))
          )
       );
    
       //Warte auf den Eingabe-Thread
       reader.join();
    
       //Gib aus, was die Hardware an Parallelität verträgt
       std::cout << std::thread::hardware_concurrency() << std::endl;
    }
    

    Erläuterung
    Da std::thread mithilfe von Variadic Templates implementiert ist, können beliebige Funktionen und Funktionsobjekte als Eingangspunkte für einen Thread definiert werden. Sowohl freistehende Funktionen wie bar als auch Elementfunktionen wie MyThread::start und sogar Lambda-Funktionsobjekte (siehe dazu den ersten Teil der Reihe) können als eigener Thread ausgeführt werden. Das Interface von std::thread erinnert ein bisschen an die entsprechende Boost-Bibliothek.

    Beispiel 2 - Einmalige Initialisierung

    #include <thread>
    
    #include <array>
    
    std::once_flag init_once;
    void general_init_routine (int data)
    {
       std::cout << "Führe allgemeine Initialisierungsaufgaben (Singletons?) durch\n";
    }
    
    void thread_fun (int x)
    {
       std::call_once (init_once, general_init_routine, x);
    
       //general_init_routine wurde mit Sicherheit ausgeführt und beendet.
    }
    
    int main ()
    {
       array<thread, 3> threads =
       {
          thread {thread_fun, 1},
          thread {thread_fun, 2},
          thread {thread_fun, 3}
       };
    
       //Warte auf alle Threads
       for (auto thread: threads)
          thread.join();
    }
    

    Erläuterung
    In diesem Beispiel werden drei Threads erstellt, die jeweils versuchen, eine gemeinsame Initialisierungsfunktion aufzurufen. Mithilfe von std::once_flag und std::call_once ist sichergestellt, dass diese Funktion tatsächlich nur ein einziges Mal aufgerufen wird. Alle anderen Threads müssen mit ihrer Ausführung außerdem warten, bis diese Funktion auch tatsächlich aufgerufen wurde. In diesem Beispiel könnte general_init_routine also entweder mit 1, 2 oder 3 aufgerufen werden.

    Status
    Die Multithreading-Bibliothek ist Teil des aktuellen Working-Drafts.

    Unterstützung
    Die Boost-Bibliothek Boost.Threads ist ähnlich aufgebaut, jedoch nicht vollständig kompatibel mit der C++09-Multithreading-Bibliothek. Da std::thread sehr stark auf neue Sprachfeatures baut, und diese nur von wenigen Compilern (in der Form nur von GCC 4.3) unterstützt werden, ist derzeit keine Implementierung verfügbar.

    Proposals
    N2139 - Hintergründe
    N2410 - Thread-Safety in der Standardbibliothek
    N2427 - Atomare Operationen
    N2480 - Eine informelle Erklärung des neuen C++-Speichermodells
    N2497 - Die Multithreading-Bibliothek

    2.2 Mutexe und Monitore

    Mutexe und Monitore dienen der sicheren Verwendung von Daten zwischen mehreren Threads. Sie verhindern Wettlaufsituationen, das sind Situationen, in denen das Ergebnis einer Operation vom zeitlichen Verhalten bestimmter Einzelsituationen abhängt. Ein sicherer Umgang mit ihnen verhindert außerdem Deadlocks, das sind Situationen, in denen Threads auf Informationen warten, die sie jedoch erst durch einen jeweils anderen Thread bekommen würden. Da dieser andere Thread jedoch selbst auf Informationen wartet (zyklische Abhängigkeit), stoppt das Programm.

    Beispiel 1 - Mutexe und Locks

    #include <thread>
    #include <mutex>
    
    #include <iostream>
    #include <array>
    #include <string>
    
    std::mutex input_mutex, output_mutex;
    
    void thread_fun ()
    {
       for (;;)
       {
          std::string txt;
          {
             std::lock_guard<std::mutex> lock {input_mutex};
             std::cin >> txt;
          }
          {
             std::lock_guard<std::mutex> lock {output_mutex};
             std::cout << txt << '\n';
          }
       }   
    }
    
    int main ()
    {
       std::array<std::thread, 2> threads =
       {
          std::thread {thread_fun},
          std::thread {thread_fun}
       };
    
       for (auto thread: threads)
          thread.join();
    }
    

    Erläuterung
    Mutexe ermöglichen die sichere Verwendung von Objekten, auf die mehrere Threads gleichzeitig zugreifen wollen. Dazu bieten sie die Funktionen lock , try_lock und unlock an. Eine bestimmte Art von Mutexen, std::timed_mutex und std::recursive_timed_mutex bieten zusätzlich die Möglichkeit, die Ressource nur für eine bestimmte Zeitspanne zu blockieren.
    Das automatische Sperren und entsperren eines Mutex (RAII) geschieht mit Hilfe der beiden Lock-Klassen std::lock_guard und std::unique_lock , wobei std::unique_lock am ehesten dem boost::scoped_locked entspricht.
    Mit den Standard-Funktionen std::try_lock und std::lock ist es zudem möglich, eine beliebige Anzahl von Locks auf eine sichere Weise gleichzeitig zu sperren.

    Beispiel 2 - Monitore

    #include <thread>
    #include <mutex>
    #include <condition_variable>
    
    #include <queue>
    #include <iostream>
    
    std::mutex guard;
    std::condition_variable monitor;
    std::queue<int> data;
    
    void produce_data ()
    {
       for (;;)
       {
          int x;
          std::cin >> x;
    
          //Threadsicheres Speichern der neuen Daten
          std::lock_guard<std::mutex> lock {guard};
          data.push_back(x);
    
          //Signalisiere, dass neue Daten da sind
          if (data.size())
             monitor.notify_one();
       }
    }
    
    void consume_data ()
    {
       for (;;)
       {
          int x;
    
          {   
             //Versuche, Daten zu lesen
             std::unique_lock<std::mutex> lock {guard};
    
             //Warte, bis neue Daten ankommen
             monitor.wait(lock, [&]()(!data.empty()));
    
             //Daten sind verfügbar
             x = data.front();
             data.pop();
          }
    
          std::cout << "Eingabe: " << x << std::endl;      
       }
    }
    
    int main ()
    {
       std::thread producer {produce_data};
       std::thread consumer {consume_data};
    
       producer.join();
       consumer.join();
    }
    

    Erläuterung
    Der Name condition_variable rührt daher, dass eine bestimmte Bedingung erfüllt sein muss, bis ein Thread seine Arbeit wieder aufnimmt: Diese Bedingung stellt in obigem Beispiel die Lambda-Funktion [&]()(!data.empty()) dar. Der aktuelle Thread soll also solange warten, bis data.empty() nicht mehr true liefert, also solange bis neue Daten eingetroffen sind. Diese Benachrichtigung erhält er durch den anderen Thread ( monitor.notify_one() ).
    wait entsperrt gleichzeitig den Lock, so dass der Producer-Thread die queue mit neuen Daten füllen kann. Sobald die Bedingung für wait erfüllt ist, wird der Lock allerdings wieder hergestellt.

    Status
    Siehe 2.1

    Unterstützung
    Siehe 2.1

    Proposals
    N2406 - Hintergründe zu Mutexen, Locks und Monitoren

    3 Datum und Zeit

    Da die C++-Multithreading-Bibliothek auch Funktionen wie Sleep und zeitabhängiges Warten auf Ressourcen ermöglichen soll, musste für sie eine eigene Date-Time-Bibliothek geschaffen werden.
    Die meisten Funktionen sind bereits in der Boost Date-Time-Bibliothek implementiert. Die Date-Time-Bibliothek, die in C++09 inkludiert werden soll, sieht sich dabei als Vorgängerin für eine noch umfassendere Bibliothek im Technical Report 2.

    Beispiel 1 - Date-Time in Verbindung mit Threads

    #include <date_time>
    #include <thread>
    
    template <class Lock>
    void thread (Lock lock)
    {
       std::this_thread::sleep(std::seconds(1));
    
       std::recursive_timed_mutex rtm;
       rtm.time_lock(std::milliseconds(20));
    }
    

    Erläuterung
    Die Date-Time-Bibliothek führt drei Arten von Typen ein:

    • Zeitpunkte. Ein Zeitpunkt repräsentiert einen Moment im Zeitkontinuum.
    • Dauer. Eine Dauer ist nicht von einem Zeitpunkt abhängig und repräsentiert eine gewisse Zeitlänge. Diese kann auch negativ sein.
    • Uhren. Ein Uhren-Typ ist ein Interface zu einem Gerät, das einen Zeitpunkt zurückgibt.

    Die konkreten Typen in C++09 sollen sein:

    • utc_time - ein Zeitpunkt in einer Auflösung von Nanosekunden.
    • hiresolution_clock - Eine Uhr, die die momentane utc_time zurückgibt.
    • hours, minutes, seconds, milliseconds, microseconds und nanoseconds - Typen, die eine Zeitdauer repräsentieren.

    Mit diesen Typen kann man intuitiv arbeiten.

    Beispiel 2 - Intuitives Interface

    #include <date_time>
    using namespace std;
    
    int main ()
    {
       utc_time now = hiresolution_clock::universal_time();
       utc_time tomorrow = now + hours(24);
    
       hours aday {24};
       aday += minutes {10};//Kompilierfehler: Verlust an Genauigkeit
    
       minutes m {10};
       m += aday;          //OK, Stunden können in Minuten umgerechnet werden
    }
    

    Erläuterung
    Ein Objekt vom Typ utc_time , das von seinem Standardkonstruktor initialisiert wird, repräsentiert die UTC-Zeit 1970-01-01 00:00:00.000000000.

    Status
    Die Date-Time-Library wurde gleichzeitig mit der Multithreading-Bibliothek in den Working-Draft aufgenommen. Allerdings wurde sie mittlerweile wieder aus dem Working-Draft entfernt, da zu viele Details nicht geklärt werden konnten. Eine Erklärung für ihr Verschwinden findet sich hier.

    Unterstützung
    Mir sind keine Compiler bekannt, die diese Features unterstützen.

    Proposals
    N2411 - Date/Time für C++0x
    N2492 - Date/Time als Teil der Multithreading-Bibliothek
    N2498 - Custom Time Duration Support

    4 System-Fehler

    Vor allem im Hinblick auf die Dateisystem-Bibliothek, die mit dem Technical Report 2 eingeführt werden soll, ist es nötig, Fehlermeldungen des Systems in C++-Ausnahmen zu verwenden. Aber auch die Multithreading-Bibliothek profitiert davon, System-Fehlermeldungen über eine standardisierte Schnittstelle zu präsentieren. Der Standard stützt sich dabei auf die POSIX-Fehlerflags.

    *Beispiel 1 - Verwendung von system-error *

    #include <system_error>
    #include <thread>
    #include <iostream>
    
    void foo () {}
    
    int main ()
    {
       try
       {
          std::thread thr { foo };
       }
       //Wenn der Thread nicht gestartet werden konnte...
       catch (std::system_error& error)
       {
          std::cerr << error.what();
       }
    }
    

    Beispiel 2 - Eigene Erweiterungen

    #include <system_error>
    
    namespace std
    {
       struct my_error_catalog : virtual public std::error_catalog
       {
          typedef std::error_catalog base_type;
    
          static value_type const out_of_ink = 20001;
          static value_type const out_of_paper = 20002;
    
          virtual auto
          is_valid_value (value_type v) const throw () -> bool
          {
             if (!base_type::is_valid_value(v))
                return v == out_of_ink || v == out_of_paper;
             return true;
          }
    
          virtual auto
          str (value_type v) const throw () -> char const*
          {
             char const* ink = "Out of Ink";
             char const* paper = "Out of Paper";
             if (base_type::is_valid_value(v)) return base_type::str(v);
             switch (v)
             {
             case out_of_ink:   return ink;
             case out_of_paper: return paper;
             default: return nullptr;
             }
          }
    
          virtual auto
          last_value() const throw() -> value_type const
          { return out_of_paper; }
       }
    }
    //...
    
    if (printer.ink_level() < 5percent)
    {
       throw system_error { std::my_error_catalog::out_of_ink, my_error_catalog() };
    }
    

    Erläuterung
    Eine der Stärken der System-Error Schnittstelle ist es, std::locales zu unterstützen. Die Vorgabe lautet außerdem, dass die Nachrichten, die what liefert, eine Fehler-Diagnose erleichtern sollen.

    Status
    Dieses Feature ist Teil des Working-Drafts. Allerdings gibt es bereits Vorschläge, die Schnittstelle zu modifizieren bzw. zu erweitern. So wie es aussieht sind auch hier einige Unklarheiten aufgetreten, und es existiert bereits der Vorschlag, system_error wieder aus dem Working-Draft zu entfernen, wie bereits mit der Date/Time-Bibliothek geschehen.

    Unterstützung
    Mir sind keine Compiler bekannt, die dieses Feature unterstützen.

    Proposals
    N2303 - System-Error-Unterstützung
    N2538 - Entfernen der System-Error-Unterstützung

    5 Integration neuer Sprachfeatures

    Durch die vielen neuen Möglichkeiten, die C++09 bietet – von Variadic Templates, Rvalue-Referenzen bis zur Unicode-Unterstützung – war es ebenso nötig, bereits bestehende Bibliotheken dem Wandel der Sprache anzupassen. Neben der Einführung von Konzepten und Concept-Maps für die Klassen der STL und der Implementierung von Move-Semantik durch Rvalue-Referenzen, die weitgehend unsichtbar im Hintergrund arbeitet, gibt es auch einige durchaus sichtbare und erwähnenswerte Features, die das Arbeiten mit der "alten" Standardbibliothek erleichtern sollen.

    5.1 Standard-Concepts

    Um das Rad nicht ständig neu erfinden zu müssen, definiert die Standard-Bibliothek einige Konzepte wie LessThanComparable, die häufig gebraucht werden. Waren es einst bestimmte Bedingungen, die nur formal an Konzepte wie Iteratoren, Container usw. gestellt wurden, sind diese nun auch explizit im Code formulierbar.

    Beispiel

    #include <concepts>
    
    struct MyOrder
    {
       int i;
    };
    
    bool operator == (MyOrder const& a, MyOrder const& b)
    {
       return a.i == b.i;
    }
    bool operator < (MyOrder const& a, MyOrder const& b)
    {
       return a.i < b.i;
    }
    
    template <typename T>
    requires EqualityComparable<T> && LessThanComparable<T>
    void foo (T a, T b)
    {
       a == b;
       a != b; //automatisch generiert - Definition in EqualityComparable
       a >= b; //automatisch generiert - Definition in LessThanComparable
    }
    
    struct MyInteger
    {
       long long value;
    
       MyInteger (long long v) : value(v) {}
    
       MyInteger& operator+= (MyInteger x) { value += x.value; return *this; }
    
       //...
    };
    
    template <Arithmetic T>
    void bar ()
    {
       T a{1}, b{2}, c{3};
       a = b + c;  //operator+ wird automatisch erzeugt.   
    }
    

    Status
    Da concepts selbst noch nicht im Working-Draft enthalten sind, ist auch der Header <concepts> noch nicht mit dabei. Bis zur Veröffentlichung des neuen Standards wird sich dies jedoch mit Sicherheit noch ändern.

    Unterstützung
    ConceptGCC unterstützt die <concepts> -Bibliothek. Allerdings kann ConceptGCC noch nicht sogenannte "Default-Implementierungen" aus concepts erstellen. (D.h. die obigen Beispiele funktionieren nicht.)

    Proposals
    N2036 - Hintergründe
    N2037 - Concepts für die Standardbibliothek (Einführung)
    N2572 - Die wichtigsten Concepts

    5.2 Noch mehr: std::string

    Die Funktionalität rund um std::string wird noch weiter ausgebaut, um ihn endgültig zur Standard-Stringklasse für C++ zu machen und von den lästigen nullterminierten Char-Zeigern wegzukommen.

    *Beispiel 1 - Schnittstellen für std::string *

    #include <locale>
    #include <fstream>
    #include <string>
    
    int main ()
    {
       std::string loc { "fr_FR.UTF-8" };
       std::locale myLocale { loc };  //Statt loc.c_str()
    
       std::string filename { "foo.txt" };
       std::fstream file { filename };//Dito hier
    
       file.close();
       //Auch filebufs unterstützen jetzt std::strings
       file.rdbuf()->open(filename, ios::out); 
    }
    

    Erläuterung
    Mit diesem Feature wird das Interface der Standardbibliothek vereinheitlicht und anfängerfreundlicher gestaltet.

    *Beispiel 2 - operator<< *

    #include <string>
    
    int main ()
    {
       std::string a, b { "strings" };
    
       a << "Aneinander" << 'r' << "eihen von " << b;
    }
    

    Erläuterung
    Das Konkatenieren von Strings wird somit übersichtlicher als die Alternative mit operator+= . Allerdings wird operator<< nicht die Konversion von anderen Typen nach std::string ermöglichen.

    Beispiel 3 - neue Stringfunktionen

    #include <stdexcept>
    #include <iostream>
    #include <string>
    
    int main ()
    {
       std::string number;
       cin >> number;
    
       try
       {
          long twice = std::stol(number) * 2;
          double half = std::stod(number) / 2;
    
          string output = "Zweimal " + number + " ist " + to_string (twice) + ", die Hälfte davon ist " + to_string(half);
       }
       catch (std::invalid_argument const&)
       {
          //Konnte number nicht in long oder double konvertieren
       }
       catch (std::out_of_range const&)
       {
          //Number war zu groß/klein für long oder double
       } 
    }
    

    Erläuterung
    Mit den Standardfunktionen std::stoi , std::stol , std::stoul , std::stoll , std::stoull , std::stof , std::stod , std::stold und std::to_string wird endlich "von Zahl nach String und zurück" ohne Stringstreams ermöglicht.

    Status
    Die neuen Schnittstellen für std::strings sind bereits im Working-Draft enthalten. Andere Erweiterungen, die von Neuerungen in der Sprache selbst abhängen, werden frühestens dann inkludiert, wenn das entsprechende Feature in den Working-Draft kommt. operator<< ist leider noch nicht im Working-Draft und es ist nicht sicher, ob er es noch rechtzeitig in den Standard schafft. Die numerischen Konvertierungsfunktionen sind bereits Teil des Working-Drafts.

    Proposals
    N1981 - Einheitliche Verwendung von std::string
    N2233 - basic_string operator<<

    6 Erweiterungen für den TR2 und danach

    Während noch fleißig am kommenden Standard gearbeitet wird, lässt sich auch schon ein bisschen was über den Technical Report 2 sagen: Unterstützung für Dateisystem-Operationen wird definitiv darin enthalten sein. Anderes, wie eine Netzwerkbibliothek, befindet sich noch in einer sehr frühen Planungsphase, auch wenn das Komitee den Neuerungen gegenüber recht aufgeschlossen ist. Das liegt wohl auch hauptsächlich daran, dass die meiste Arbeitszeit nun in den kommenden Standard fließt und nicht in spekulative Erweiterungen danach.

    6.1 Dateisystem

    Die Dateisystem-Bibliothek, die in den Technical-Report 2 aufgenommen werden soll, orientiert sich stark an Boost.Filesystem.

    Beispiel

    #include <iostream>
    #include <filesystem>
    
    using std::tr2::sys;
    using std::cout;
    
    int main (int argc, char** argv)
    {
       std::string p { argc <= 1 ? "." : argv[1] };
    
       if (is_directory(p))
       {
          for (directory_iterator iter(p); iter != directory_iterator(); ++iter)
          {
             cout << iter->path().leaf() << ' '; //Dateinamen anzeigen
             if (is_regular(iter->status()))
                cout << file_size(iter->path());
             cout << '\n';
          }
       }
       else cout << (exists(p) ? "Found: " : "Not Found: ") << p << '\n';
    }
    

    Status
    Dieses Feature ist bereits als Teil des TR2 angenommen worden. Der TR2 steht allerdings noch weit vor der Veröffentlichung.

    Unterstützung
    Mir sind keine Compiler bekannt, die dieses Feature unterstützen. Allerdings funktioniert Boost.Filesystem ähnlich.

    Proposals
    N1975 - Quelle und Erläuterung des Beispiels

    6.2 Weitere mögliche Neuerungen

    Über weitere Bibliotheken, die in den TR2 aufgenommen werden sollen, lassen sich im Moment nur Vermutungen aufstellen. Möglicherweise wird die Boost.Asio-Bibliothek für C++-Netzwerkfähigkeiten zuständig sein, Boost.Any könnte ebenso aufgenommen werden.

    Sollten Ihnen als LeserInnen Bibliotheken einfallen, die Sie für besonders aufnahmenswert empfinden, folgen Sie dem "Call for Proposals" - Wer weiß, vielleicht kommt die eine oder andere Idee auch durch :).

    Proposals
    N2175 - Proposal für eine Netzwerkbibliothek
    N1393 - Any Library Proposal
    N2044 - Shared Memory Proposal
    N2276 - Thread Pools und Futures

    7 Ausblick

    In den folgenden Artikeln dieser Reihe beschäftige ich mich eingehender mit bestimmten Features, zu denen ich nun einen groben Überblick gegeben habe. Die Auswahl, nach der ich vorgehe, ist dabei einfach:

    Bereits verfügbare Features zuerst, bevorzugt Änderungen, die einen großen Einfluss auf den zukünftigen Programmierstil haben werden.

    Die Wahl fällt damit zunächst auf Variadic Templates, weil diese das nächste große Feature sind, das bereits implementiert ist und weil es wahrscheinlich ist, dass es in naher Zukunft von mehreren Compilern unterstützt wird, da es nicht sonderlich schwer zu implementieren ist.

    8 Verweise

    ISO - C++ Library Working Group (LWG) Status Report
    Implementierungsstatus der libstdc++
    C++0X - The New Face of Standard C++
    Daraus: Minimal Garbage Collection Support - The Latest Proposal (I-III)
    N2151 - Variadic Templates für die Standardbibliothek



  • Sehr schön. 👍
    Und vielen Dank für die ganze Mühe. 😉



  • Interessant,

    Danke!



  • Schöner Artikel 👍

    Für die System-Fehler (4.) hat boost 1.35 bereits eine Implementierung: http://www.boost.org/doc/libs/1_35_0/libs/system/doc/index.html



  • also erstmal spitzen artikel und vielen dank für die viele mühe, die du dir mit deinen artikeln gibst.

    eine frage zu funktionsobjekten. in deinem code steht ein bsp zum vertauschen der parameter einer funktion:

    function<void (int)> foo_reversed = bind (foo_binary, placeholders::_2, placeholders::_1);
    

    müsste das nicht

    function<void (int, int)> foo_reversed = bind (foo_binary, placeholders::_2, placeholders::_1);
    

    heißen, da die neue funktion zwei int als parameter nimmt?

    btw. könntest du bitte noch einen hinweis einfügen, wie sich das mit den placeholders gestaltet? beim ersten lesen habe ich eine ganze weile gestutzt, was du da gemacht hast.



  • ja, du hast da natürlich recht. ich habe auch eine etwas ausführlichere erklärung eingefügt.



  • edit: sorry ganz vergessen: vielen dank für die korrekturen und vor allem bin dankbar, dafür dasss ich dir hier nochmal ausdrücklich für deine mühe danken werde. ich hätte keine zeit gehabt, mich durch den vollständigen draft zu lesen, aber deine mühe erspart mir diese tätigkeit. es hat mich sich selten ein artikel wie deine hier interessiert. du bist ein brückenbauer aka pontifex.
    da ich jetzt gerade den draft zu dem placeholders las. der spruch "M is the implementation-defined number of placeholders" ist irgendwie ein bisschen blöd. ich hoffe einfach mal, dass jeder compilerbauer m hinreichend groß ansetzt.
    wenn ich jetzt gerade nichts überlas, ist nämlich keine möglichkeit vorgesehen, um m abzufragen. das wäre extrem lästig.



  • 👍 danke



  • ghorst schrieb:

    da ich jetzt gerade den draft zu dem placeholders las. der spruch "M is the implementation-defined number of placeholders" ist irgendwie ein bisschen blöd. ich hoffe einfach mal, dass jeder compilerbauer m hinreichend groß ansetzt.
    wenn ich jetzt gerade nichts überlas, ist nämlich keine möglichkeit vorgesehen, um m abzufragen. das wäre extrem lästig.

    Im Anhang steht dazu folgendes:

    2 The number of distinct placeholders ([3.2]) is implementation defined. The number should be at least 10.

    10 Stück sollten normalerweise völlig ausreichen.



  • Beim Juni-Meeting wurde außerdem noch eine Bibliothek für Zeitpunkte und Zeitdauern eingeführt
    http://www.open-std.org/jtc1/sc22/wg21/docs/papers/2008/n2661.htm


Anmelden zum Antworten