Aus Linux-Magazin 12/2015

Modernes C++ in der Praxis – Folge 25

C++ besitzt zwar bereits einen reichen Satz an Containern. C++11 erweitert das Portfolio dennoch: Zu den vier neuen assoziativen Containern gesellen sich noch drei sequenzielle.

Neben den assoziativen [1] bescheren auch sequenzielle Container C++-Entwicklern zusätzliche Möglichkeiten. So bringt C++11 neuerdings ein »std::array« mit, das die Vorteile des C-Array mit denen des »std::vector« vereint. Die doppelt verkettete Liste »std::list« des klassischen C++ haben die Entwickler von C++11 um die einfach verkettete Liste »std::forward_list« erweitert. Das »std::pair« erhält mit »std::tuple« einen großen Bruder.

Die Vorteile der drei sequenziellen Container lassen sich schnell erklären: »std::forward_list« und »std::array« sind auf minimalen Speicherbedarf getrimmt, während »std::tuple« ein heterogener Container fester Länge ist, der das »std::pair« -Interface stark ausbaut.

Als homogener Container fester Länge vereint »std::array« das Beste aus zwei Welten: Zum einen erbt der Container die Speicher- und Laufzeiteffizienz des C-Array, zum anderen das Interface von »std::vector« . Das heißt insbesondere, dass »std::array« auch die Algorithmen der Standard Template Library (STL) unterstützt.

Listing 1 stellt den intuitiven Umgang mit dem sequenziellen Container vor (Details auf Cppreference.com [2]). In Zeile 9 initialisiert eine Initialisiererliste ein Array der Länge 8 und gibt es mit Hilfe der Lambda-Funktion in der Folgezeile aus. Bindet die Lambda-Funktion seine aus dem umgebenden Kontext verwendete Variable »sum« über die Referenz »[&sum](int v) { sum += v; }« , kann diese Referenz als Summationsvariable des STL-Algorithmus »std::for_each« dienen (Zeilen 15 und 16).

Listing 1

std::array

01 #include <algorithm>
02 #include <array>
03 #include <iostream>
04
05 int main(){
06
07   std::cout << std::endl;
08
09   std::array <int,8> array1{1,2,3,4,5,6,7,8};
10   std::for_each(array1.begin(),array1.end(),[](int v){std::cout << v << " ";});
11
12   std::cout << std::endl;
13
14   int sum = 0;
15   std::for_each(array1.begin(), array1.end(),[&sum](int v) { sum += v; });
16   std::cout << "sum of array{1,2,3,4,5,6,7,8}: " << sum << std::endl;
17
18   std::for_each(array1.begin(), array1.end(),[](int& v) { v=v*v; });
19   std::for_each( array1.begin(),array1.end(),[](int v){std::cout << v << " ";});
20
21   std::cout << "\n\n";
22 }

In Zeile 18 agiert die Lambda-Funktion »[](int& v) { v=v*v; }« per Referenz auf den Elementen des »std::array« . Das ermöglicht es »std::for_each« , die Elemente des Array on the Fly zu modifizieren, in Abbildung 1 ist die Ausgabe des Programms zu sehen.

Abbildung 1: Die Lambda-Funktion agiert über eine Referenz auf den Elementen von »std::array«.

Abbildung 1: Die Lambda-Funktion agiert über eine Referenz auf den Elementen von »std::array«.

Besonderheiten

Zwei Besonderheiten sollte der Entwickler beim »std::array« im Kopf behalten. Zum einen kann er die Größe des sequenziellen Containers zur Laufzeit nicht anpassen. Zum anderen gelten für das Initialisieren seiner Elemente besondere Regeln:

  • »std::array<int,10> arr« initialisiert die Elemente nicht.
  • »std::array<int,10> arr{}« initialisiert die Elemente standardmäßig.
  • »std::array<int,10> arr{1,2,3,4,5}« initialisiert die restlichen Elemente standardmäßig.

Der Programmierer greift auf verschiedene Weisen auf die Elemente des Array »arr« zu. Er verwendet wahlweise die Methode »arr.at(4)« , die Array-Grenzen überprüft. Alternativ greift er zum Indexoperator »arr[4]« , der das nicht tut. Mit »std::get<4>(arr)« besteht eine weitere Option, die Containergrenzen zu überprüfen und so das fünfte Element des »std::array« zu adressieren. Die Syntax deutet die Verwandtschaft von »std::array« mit »std::tuple« an.

Der große Bruder

Beim »std::tuple« handelt es sich um einen heterogenen Container fester Länge. Der erlaubt es, beliebig zwischen zwei-elementigen Tupeln und Paaren zu konvertieren. Neben dem Konstruktor besitzt »std::tuple« die freie Funktion »std::make_tuple()« , die ein Tupel direkt erzeugt. Mit der freien Funktion »std::get()« liest der Entwickler die Elemente eines Tupels auf umständliche Weise aus, kann diese aber ändern.

Listing 2 demonstriert das Tupel in Aktion. Der »std::make_tuple()« -Aufruf in Zeile 10 lässt sich dank einer automatischen Typableitung mit »auto« weiter vereinfachen:

Listing 2

std::tuple

01 #include <iostream>
02 #include <string>
03 #include <tuple>
04
05 int main(){
06
07   std::cout << std::boolalpha << std::endl;
08
09   std::tuple<std::string,int,float> tup1("first",3,4.17);
10   std::tuple<std::string,int,double> tup2= std::make_tuple("second",4,1.1);
11
12   std::cout << "tup1: "  << std::get<0>(tup1) << "," << std::get<1>(tup1) << "," << std::get<2>(tup1) << std::endl;
13   std::cout << "tup2: "  << std::get<0>(tup2) << "," << std::get<1>(tup2) << "," << std::get<2>(tup2) << std::endl;
14
15   std::cout << "tup1 < tup2: " << (tup1 < tup2) << std::endl;
16
17   std::cout << std::endl;
18
19   std::get<0>(tup2)= "Second";
20
21   std::cout << "tup1: "  << std::get<0>(tup1) << "," << std::get<1>(tup1) << "," << std::get<2>(tup1) << std::endl;
22   std::cout << "tup2: "  << std::get<0>(tup2) << "," << std::get<1>(tup2) << "," << std::get<2>(tup2) << std::endl;
23
24   std::cout << "tup1 < tup2: " << (tup1 < tup2) << std::endl;
25
26   std::cout << std::endl;
27 }
auto tup2= std::make_tuple("second",4,1.1)

Vergleicht das C++-Programm mehrere »std::tuple« -Container, etwa wie in den Zeilen 15 und 24, so findet dieser Vergleich auf den Elementen des Tupels statt. Neben »<« unterstützt »std::tuple« die Vergleichsoperatoren »==« , »!=« , »>« , »<=« und »=>« , Abbildung 2 zeigt seine Ausgabe.

Abbildung 2: Tupelvergleiche zwischen mehreren Containern finden in der Regel auf den Elementen von »std::tuple« statt.

Abbildung 2: Tupelvergleiche zwischen mehreren Containern finden in der Regel auf den Elementen von »std::tuple« statt.

C++14 adressiert die Elemente eines Tupels nicht nur über den Index, sondern auch über den Typ seiner Elemente. Dazu muss dieser genau einmal im Tupel vorkommen, da andernfalls der Compiler mit einer Fehlermeldung protestiert. So gibt der Aufruf »std::get<int>(tup1)« »3« zurück.

Aber »std::tuple« [3] kann noch deutlich mehr. Die freie Funktion »std::tie()« beispielsweise erzeugt Tupel, deren Elemente per Referenz an ihr Argument gebunden werden. Über die Funktion »std::tie()« kann der Entwickler auch explizit Elemente des Tupels ignorieren. Listing 3 wirft einen gezielten Blick auf die Funktion.

Listing 3

std::tie()

01 #include <functional>
02 #include <iostream>
03 #include <tuple>
04
05 int main(){
06
07   std::cout << std::endl;
08
09   int first= 1;
10   int second= 2;
11   int third= 3;
12   int fourth= 4;
13
14   auto tup= std::tie(first,second,third,fourth)= std::make_tuple(1001,1002,1003,1004);
15
16   std::cout << "tup: (" << std::get<0>(tup) << "," << std::get<1>(tup) << "," << std::get<2>(tup) << "," << std::get<3>(tup) << ")" << std::endl;
17   std::cout << "variables: " << first << " " << second << " " << third << " " << fourth << std::endl;
18
19   std::cout << std::endl;
20
21   first= 1;
22   std::cout << "std::get<0>(tup): " << std::get<0>(tup) << std::endl;
23
24   std::cout << std::endl;
25
26   int a;
27   int b;
28
29   std::tie(std::ignore,a,std::ignore,b)= tup;
30
31   std::cout << "a: " << a << std::endl;
32   std::cout << "b: " << b << std::endl;
33
34   std::cout << std::endl;
35 }

Der Ausdruck in Zeile 14 entpuppt sich beim ersten Hinsehen als ziemlich schwer zu entziffern. Am einfachsten lässt er sich wohl von rechts nach links lesen. So erzeugt »std::make_tuple(1001,1002,1003,1004)« zunächst ein temporäres Tupel. Die Funktion »std::tie()« bindet dessen Elemente per Referenz an die Variablen »first« bis »fourth« . Zeile 16 weist der Variablen »tup« das temporäre Tupel zu.

Setzt das Listing die Variable »first« wieder auf »1« , verändert dies auch das nullte Element des Tupels (siehe Zeile 22). Dank »std::ignore« (Zeile 29) ignoriert der Code beim Einsatz von »std::tie()« die Tupelelemente. Abbildung 3 stellt das Programm in Aktion dar.

Abbildung 3: Die Hilfsfunktion »std::tie()« bindet in <a href="#article_l3" class="listing" title=

Listing 3 ein temporäres Tupel per Referenz an Variablen.” width=”300″ height=”193″ /> Abbildung 3: Die Hilfsfunktion »std::tie()« bindet in Listing 3 ein temporäres Tupel per Referenz an Variablen.

Nur einfach verkettet

Die einfach verkettete Liste »std::forward_list« ähnelt der doppelt verketteten »std::list« , bietet aber ein deutlich eingeschränkteres Interface an. So unterstützt »std::forward_list« weder das Dekrementieren eines Iterators, noch kennt sie ihre Länge. Zudem lässt sich nur in eine Richtung über »std::forward_list« iterieren, wie es auch Abbildung 4 nahelegt. Operationen auf ihr betreffen lediglich den Anfang oder die Position nach der aktuellen. Dafür ist die einfach verkettete Liste auf minimale Speicheranforderungen getrimmt.

Abbildung 4: Die Struktur der einfach verketteten Liste »std::forward_list« stellt minimale Speicheranforderungen, lässt sich aber nur in eine Richtung iterieren.

Abbildung 4: Die Struktur der einfach verketteten Liste »std::forward_list« stellt minimale Speicheranforderungen, lässt sich aber nur in eine Richtung iterieren.

Zu den weiteren Eigenschaften von »std::forward_list« gehört, dass sie

  • keinen wahlfreien Zugriff kennt. Für einen Zugriff auf ein beliebiges Element ist in der Regel die Iteration über jedes Element notwendig;
  • Elemente direkt hinzufügen oder löschen kann, falls deren Position bekannt ist;
  • zusichert, dass die Iteratoren nach dem Hinzufügen oder Löschen eines Elementes gültig bleiben.

Listing 4 dröselt den besonderen Umgang mit der einfach verketteten Liste »std::forward_list« in ihrem natürlichen Habitat auf. Sämtliche Feinheiten ergänzt die CPP-Reference [4].

Listing 4

std::forward_list

01 #include <algorithm>
02 #include <forward_list>
03 #include <iostream>
04
05 int main(){
06
07   std::cout << std::boolalpha << std::endl;
08
09   std::forward_list<int> myForList;
10
11   std::cout << "myForList.empty(): "  << myForList.empty() << std::endl;
12   myForList.push_front(7);
13   myForList.push_front(6);
14   myForList.push_front(5);
15   myForList.push_front(4);
16   myForList.push_front(3);
17   myForList.push_front(2);
18   myForList.push_front(1);
19
20   std::cout << std::endl;
21
22   std::cout << "myForList: " << std::endl;
23   for (auto i: myForList) std::cout << i << " ";
24   std::cout << "\n\n";
25
26   myForList.erase_after(myForList.before_begin());
27   std::cout<< "myForList.front(): " << myForList.front() << "\n\n";
28
29   std::forward_list<int>myForList2;
30   myForList2.insert_after(myForList2.before_begin(),1);
31   myForList2.insert_after(myForList2.before_begin(),2);
32   myForList2.insert_after(myForList2.before_begin(),3);
33   myForList2.push_front(1000);
34
35   std::cout << "myForList2: " << std::endl;
36   for (auto i: myForList2) std::cout << i << " ";
37   std::cout << "\n\n";
38   auto IteratorTo5= std::find(myForList.begin(),myForList.end(),5);
39   myForList.splice_after(IteratorTo5,std::move(myForList2));
40
41   std::cout << "myForList: " << std::endl;
42   for (auto i: myForList) std::cout << i << " ";
43   std::cout << "\n\n";
44
45   myForList.sort();
46
47   std::cout << "myForList: " << std::endl;
48   for (auto i: myForList) std::cout << i << " ";
49   std::cout << "\n\n";
50
51   myForList.reverse();
52
53   std::cout << "myForList: " << std::endl;
54   for (auto i: myForList) std::cout << i << " ";
55   std::cout << "\n\n";
56
57   myForList.unique();
58
59   std::cout << "myForList: " << std::endl;
60   for (auto i: myForList) std::cout << i << " ";
61   std::cout << "\n";
62
63   std::cout << std::endl;
64
65 }

Die Zeilen 12 bis 18 schieben die Zahlen 1 bis 7 auf die »std::forward_list« . Der Ausdruck »myForList.erase_after(myForList.before_begin())« in Zeile 26 entfernt das erste Element der einfach verketteten Liste. Nach demselben Muster setzt das Listing die Elemente »1« , »2« , »3« und »1000« anschließend über »myForList2« jeweils an den Anfang der einfach verketteten Liste.

Als besonders interessant erweist sich in diesem Kontext der Ausdruck

myForList.splice_after(IteratorTo5,std::move(myForList2))

in Zeile 39: Er verschiebt »myForList2« nach »IteratorTo5« . Erhöhte Aufmerksamkeit verdient ebenfalls die Zeile 57. In ihr entfernt der Ausdruck »myForList.unique()« aufeinanderfolgende Duplikate. Dabei setzt »std::unique()« allerdings voraus, dass »myForList« vorsortiert ist. Abbildung 5 zeigt das Programm schließlich in Aktion.

Abbildung 5: Die einfach verkettete Liste »std::forward_list« in Aktion.

Abbildung 5: Die einfach verkettete Liste »std::forward_list« in Aktion.

Wie geht’s weiter?

Eine kleine, aber feine neue Bibliothek in C++11 sind die Referenz-Wrapper. Ein »std::reference_wrapper<T>« ist ein kopier-konstruierbarer und zuweisbarer Wrapper um ein Objekt vom Typ »T&« . Verwirrt? Das ist verständlich. Die Auflösung folgt im nächsten Artikel.

Infos

  1. Rainer Grimm, “Geschwindigkeit zählt”: Linux-Magazin 12/13, S. 100
  2. Mehr zu »std::array« : http://en.cppreference.com/w/cpp/container/array
  3. Container »std::tuple« : http://en.cppreference.com/w/cpp/utility/tuple
  4. Referenz zu »std::forward_list« : http://en.cppreference.com/w/cpp/container/forward_list

Der Autor

Rainer Grimm arbeitet als Software-Architekt und Gruppenleiter bei der Metrax GmbH in Rottweil. Besonders die Software der hauseigenen Defibrillatoren ist ihm eine Herzensangelegenheit. Seine Bücher “C++11 für Programmierer”, “C++” und “C++11-Standardbibliothek” sind bei O’Reilly erschienen.

DIESEN ARTIKEL ALS PDF KAUFEN
EXPRESS-KAUF ALS PDFUmfang: 4 HeftseitenPreis €0,99
(inkl. 19% MwSt.)
LINUX-MAGAZIN KAUFEN
EINZELNE AUSGABE Print-Ausgaben Digitale Ausgaben
ABONNEMENTS Print-Abos Digitales Abo
TABLET & SMARTPHONE APPS Readly Logo
E-Mail Benachrichtigung
Benachrichtige mich zu:
0 Kommentare
Älteste
Neuste Beste Bewertung
Inline Feedbacks
Alle Kommentare anzeigen
Nach oben