Dieser und noch weitere Artikel wurde von queer_boy erstellt.


Folgende Themen werden von diesem Artikel berührt:


Druckversion des Artikels


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:
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

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


Beispiel
C++:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
#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
C++:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
#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.

C++:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
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:

C++:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
#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:


Beispiel
C++:
1
2
3
4
5
6
7
8
9
10
11
12
13
#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
C++:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
#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
C++:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
#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
C++:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
#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
C++:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
#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
C++:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
#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
C++:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
#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
C++:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
#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
C++:
1
2
3
4
5
6
7
8
9
10
11
#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:

Die konkreten Typen in C++09 sollen sein:

Mit diesen Typen kann man intuitiv arbeiten.


Beispiel 2 - Intuitives Interface
C++:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
#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
C++:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
#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
C++:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
#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
C++:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
#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
C++:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
#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<<
C++:
1
2
3
4
5
6
7
8
#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
C++:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
#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
C++:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
#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

Sie können Kommentare zu diesem Artikel im Forum schreiben. (Eine Registrierung ist nicht notwendig.)

Logo-Design: MastaMind Webdesign