C++ Kurs

pair Datentyp

Die Themen:

Einleitung
pair Datentypen
make_pair Hilfsfunktion
Zugriff auf pair-Elemente
pair als Returnwert
pair-Operatoren
Beispiel und Übung

Einleitung

Der in der Standard Bibliothek definierte Datentyp pair dient zum Zusammenfassen von zwei Daten, deren Datentypen beliebig sein können. Mithilfe von pair können Sie damit sogar zwei Werte aus einer Funktion/Memberfunktion zurückgeben, indem Sie als Returntyp der Funktion/Memberfunktion pair verwenden. Dazu folgt später noch ein Beispiel.

Wenn Sie den Datentyp pair einsetzen, müssen Sie die Header-Datei <utility> einbinden. Außerdem liegt der Datentyp pair, wie übrigens alle Standard Bibliothek Komponenten, im Namensraum std. Beachten Sie dies bitte beim Einsatz der Komponenten! Wenn Sie dies nicht tun, erhalten eine Fehlermeldung, dass die entsprechende Komponente nicht bekannt ist.

Allen in einem pair abgelegten Elementen ist eines gemeinsam: sie sind immer eine Kopie des ursprünglichen Elements. Werden also nach dem Ablegen eines Elements in einem pair noch Änderungen an einem der Elemente vorgenommen, so wirken sich diese nicht auf das im pair abgelegte Element aus.

pair Datentypen

pair für einfache Datentypen

Um ein Objekt des Datentyps pair zu definieren, geben Sie nach dem Datentyp pair in spitzen Klammern die beiden Datentypen an, die im pair Objekt abgelegt werden sollen. Im Anschluss daran folgt dann wie üblich der Objektname. Geben Sie nach dem Objektnamen nichts weiter an, so werden die beiden Elemente (bei einfachen Datentypen wie char, int usw.) mit 0 initialisiert.


// pair für einen int und einen long
std::pair<int, long> emptyPair;
// pair für char* und float
std::pair<char*, float>  cfPair("any text", 1.2f);
// Kopieren eines pair Objekts
std::pair<char*, float> copyPair(cfPair);

Zusätzlich stellt pair noch Konstruktore bereit, um ein pair Objekt bei seiner Definition mit Werten zu initialisieren (zweite Anweisung) oder eine Kopie eines bestehenden pair Objekts zu erstellen (dritte Anweisung). Sie müssen bei der Initialisierung eines pair Objekts natürlich auf die Datentypen achten, mit denen Sie das pair Objekt initialisieren.

pair für Objekte

Außer einfache Datentypen können in einem pair Objekt auch Objekte abgespeichert werden. Diese Objekte müssen aber die folgenden Eigenschaften besitzen:

  1. Sie müssen kopierbar sein. Bei Objekten die dynamische Eigenschaften oder andere Objekte enthalten, muss die Objektklasse den Kopierkonstruktor definieren.
  2. Sie müssen zuweisbar sein, d.h. das Verhalten des Zuweisungsoperators muss definiert sein. Bei Objekten die dynamische Eigenschaften oder andere Objekte enthalten, muss die Objektklasse dazu explizit den Zuweisungsoperator = überladen.
  3. Sie müssen vergleichbar sein. Dazu muss die Objektklasse die Operatoren < und == durch friend-Funktionen überladen. Die restlichen Operatoren, wie zum Beispiel <= oder != werden dann aus diesen beiden überladenen Operatoren gebildet.

// Klasse die die pair Anforderung erfüllt
class Any
{
   ....
 public:
   // Standard-ctor für Initialisierung mit 'leerem' pair
   Any();
   // copy-ctor
   Any(const Any& CObj);
   // Überladener Zuweisungsoperator
   Any& operator = (const Any& COp2);
   // Überladener < Operator
   friend bool operator < (const Any& COp1, const Any& COp2);
   // Uberladener == Operator
   friend bool operator == (const Any& COp1, const Any& COp2);
   ....
};

Oben sehen Sie eine Klasse, die die genannten Anforderungen erfüllt. Beachten Sie bitte, dass die Vergleichsoperatoren < und == durch eine friend-Funktion überladen sind. Wie der Vergleich von pair Daten letztendlich durchgeführt wird, das sehen wir uns nachher gleich an.

Um dann ein Objekt in einem pair abzulegen, geben Sie innerhalb der spitzen Klammer die Klasse des Objekts an. Die im pair abgelegten Daten können dabei beliebig aus Objekten, Objektzeigern, Standard-Datentypen und Zeigern zusammengesetzt sein. Ja ist sogar möglich, als Datentyp ein weiteres pair zu verwenden.


// Leeres pair mit Any-Objekt und float
std::pair<Any, float> emptyPair;
// pair mit zwei Window-Objekten
std::pair<Window, Window>  winPair(Window(...), Window(...));
// pair mit string und Window-Zeiger
std::pair<string, Window*>  swPair("Mein Fenster", new Window(...));

Sehen wir uns jetzt aber einmal genauer an was vonstatten geht, wenn Sie ein pair definieren in dem Objekte abgelegt werden.


// 1. Fall: leeres pair mit anschl. Zuweisung
std::pair<Any, float> aPair;
aPair = std::pair<Any,float>(Any(...), 1.2f);
// 2. Fall: Objekt getrennt erstellen und dann im pair aufnehmen
Any myObj(...);
std::pair<Any, float> bPair(myObj, 2.0f);
// 3. Fall: Objekt bei pair Definition erzeugen
std::pair<Any, float> cPair(Any(...),3.0f);
// 4. Fall: pair Kopie anlegen
std::pair<Any, float> dPair(initPair);

1. Fall

Bei der Definition eines leeren pair Objekts wird zuerst der Standard-Konstruktor des im pair abgelegten Objekts ausgeführt, um das Objekt zu initialisieren. Bei einer anschließenden Zuweisung an das pair-Objekt wird dann zunächst ein temporäres Objekt von der im pair angegebenen Klasse erzeugt. Dieses temporäre Objekt wird per Kopierkonstruktor in ein neues, weiteres pair Objekt übertragen (das pair-Objekt, welches rechts vom Zuweisungsoperator steht). Erst dann wird dieses neue pair per Zuweisung dem bisherigem leeren pair zugewiesen, was wiederum zum Aufruf des Zuweisungsoperator des im pair abgelegten Objekts führt. Und zum Schluss wird das Objekt im pair rechts vom Zuweisungsoperator so wie das temporäre Objekt gelöscht. Sie sehen, dieses Verfahren ist relativ aufwändig.

2. Fall

Es wird zuerst das im pair abzulegende Objekt erstellt und dieses dann einem pair bei seiner Definition übergeben. Bei der pair Definition wird der Kopierkonstruktor des im pair abgelegten Objekts ausgeführt, um eine Kopie des ursprünglichen Objekts im pair abzulegen.

3. Fall

Bei der Definition eines pair wird auch gleichzeitig das darin abzulegende Objekt definiert. In diesem Fall wird zuerst ein temporäres Objekt erzeugt , das dann per Kopierkonstruktor in das pair übernommen wird. Nach der Übernahme wird dieses temporäre Objekt wieder gelöscht.

4. Fall

Wird ein pair bei seiner Definition mit einem anderen pair initialisiert, so wird der Kopierkonstruktor des Objekts aufgerufen, das im ursprünglichen pair abgelegt ist.

Die in einem pair abgelegten Objekte werden beim Löschen des pair ebenfalls gelöscht, d.h. es wird der Destruktor des im pair abgelegten Objekts automatisch aufgerufen.

Obacht geben müssen Sie, wenn Sie statt Daten oder Objekte Zeiger in einem pair ablegen. Zum einen wird, wenn Objektzeiger im pair abgelegt sind, beim Löschen des pair nicht mehr automatisch das über den Zeiger referenzierte Objekt gelöscht, sondern nur der Objektzeiger. Sie müssen das Objekt also explizit selbst (vorher!) entfernen.

Und zum anderen wird beim Duplizieren eines solchen pair nur der Zeiger dupliziert. Damit verweisen die Zeiger in beiden pair auf die gleiche Speicherstelle bzw. auf das gleiche Objekt! Das trifft auch dann zu, wenn Sie zwei solche pair einander zuweisen; auch hier werden nur die Zeiger zugewiesen. Seien Sie also vorsichtig mit pair die Zeiger enthalten!


// pair mit Objektzeiger erzeugen
std::pair<Any*, float> pPair(new Any(...), 4.0f);
...
// pair duplizieren, dupliziert Objektzeiger!
std::pair<Any*, float> cPair(pPair);

pair Felder

Von pair Objekten lassen sich auch entsprechende Felder bilden. Mithilfe des gleich beschriebenen Funktions-Templates make_pair(...) lässt sich solch ein Feld auch recht einfach initialisieren.


std::pair<std::string, float> myPairs[] = {
    std::pair<std::string,float>("Toast",1.60f),
    std::pair<std::string,float>("Butter",1.05f)
};

make_pair Hilfsfunktion

Die Standard-Bibliothek enthält das Funktions-Template make_pair(...) das es erlaubt, ein pair ohne die explizite Angabe der pair-Datentypen zu erstellen. Dazu erhält make_pair(...) lediglich die beiden Daten/Objekte als Parameter übergeben, die im pair-Objekt abgelegt werden sollen.


// Definition des pair Objekts
std::pair<char*, float> myPair;
// Zuweisung ohne make_pair(...)
myPair = std::pair<char*,float> ("Toast",1.60f);
// Zuweisung mit make_pair
myPair = std::make_pair("Toast",1.60f);

Im Beispiel wird zunächst ein leeres pair Objekt definiert. Diesem pair Objekt wird dann einmal normal ein 'Wert' zugewiesen und einmal mithilfe des make_pair(...) Funktions-Templates.

Haben Sie ein pair definiert, das ein Element vom Typ std::string enthält, so müssen Sie beim Aufruf von make_pair(...) auch ein string-Objekt angegeben und keinen C-String. Die folgende Anweisung erzeugt deshalb einen Fehler:

std::pair<std::string, int> myPair;
myPair = std::make_pair("any c-string",1);

Richtig geht's so:

std::pair<std::string, int> myPair;
myPair = std::make_pair(std::string("any string"),1);

Zugriff auf pair-Elemente

Da pair in der Standard-Bibliothek als struct-Datentyp realisiert ist (alle Elemente sind public!), kann direkt auf die beiden im pair abgelegten Werte zugegriffen werden. Das erste Element ist über den Elementnamen first erreichbar und das zwei Element über den Name second.


// pair Definition
std::pair<std::string, float> myPair("Toast",1.60f);
// Ausgabe der beiden pair-Elemente
std::cout << myPair.first << ',' << myPair.second << std::endl;

pair als Returnwert

Der Datentyp pair lässt sich auch sehr gut als Returntyp von Funktionen/Memberfunktionen einsetzen, die mehr als einen Wert zurückliefern. Sehen Sie sich dazu einmal das nachfolgende Beispiel an.


// Funktion zur Berechnung einer Wurzel
// Liefert ein pair zurück
inline std::pair<bool,double> CalcSqrt(double val)
{
   if (val < 0)
      return make_pair(false,0.0f);
   else
      return make_pair(true,sqrt(val));
}
...
// pair für Returnwert
std::pair<bool,double> result;
...
// Wurzel berechnen lassen
result = CalcSqrt(-1.44);
// Ergebnis abprüfen
if (!result.first)
   std::cout << "CalcSqrt() Fehler!";
else
   std::cout << "Wurzel: " << result.second;

Die Funktion CalcSqrt(...) dient zur Berechnung der Quadratwurzel aus einer Zahl. Als Returnwert liefert sie ein pair, in dessen ersten Element first  der Erfolg oder Misserfolg der Wurzelberechnung in Form eines bool-Werts abgelegt ist. Konnte die Wurzel erfolgreich berechnet werden, so enthält das zweite Element die Wurzel aus dem übergebenen Wert.

Beachten Sie auch, dass die Funktion CalcSqrt(...) als inline-Funktion definiert ist damit der Aufruf der Funktion keinen unnötigen Overhead erzeugt. Außerdem wird zur Erzeugung des pair Rückgabewerts die Funktion make_pair(...) verwendet.

pair-Operatoren

Der Datentyp pair überlädt unter anderem auch den Zuweisungsoperator. Der Zuweisungsoperator weist die beiden Elemente first und second einander direkt zu. Wenn Sie Objekte in einem pair ablegen, so sollte die Klasse des Objekts den Zuweisungsoperator also ebenfalls überladen.


class Any
{
   ....
   Any& operator = (const Any& rhs)
   {
      ... // Zuweisungen der Any-Elemente
   }
};
...
std::pair<int,Any> firstPair(1,Any(...));
std::pair<int,Any> secondPair;
...
// Ruft = Operator von Any auf
secondPair = firstPair;

Außer dem Zuweisungsoperator überlädt pair noch die Vergleichsoperatoren < und ==. Die restlichen Vergleichsoperatoren, wie z.B. >= und !=, werden, wie bereits erwähnt, aus diesen beiden überladenen Operatoren gebildet.

Wenn ein pair Objekte enthält und Sie die pairs miteinander vergleichen wollen, so muss die Klasse der Objekte die beiden Operatoren < und == durch friend-Funktionen überladen.


class Any
{
   ....
   friend bool operator < (const Any& lhs, const Any& rhs);
   friend bool operator == (const Any& lhs, const Any& rhs);
   
};
...
std::pair<int,Any> firstPair(1,Any(...));
std::pair<int,Any> secondPair(2,Any(...));
std::pair<Any,int> myPair(Any(...),3);
std::pair<Any,int> yourPair(Any(...),1);
...
// Vergleicht zuerst int und dann Any
if (firstPair < secondPair)
   ....
// Vergleicht zuerst Any und dann int
if (myPair != yourPair)
   ...
// Direkter Vergleich der Elemente immer möglich
if (myPair.first == yourPair.first)
   ...

Beim Vergleichen von pair Objekten hat das Element first Vorrang. D.h. nur wenn der Vergleich von first true ist, wird das Element second mit in der Vergleich einbezogen. Achten Sie deshalb bei der Definition eines pair Objekts darauf, dass die pair Elemente in der richtigen Reihenfolge definiert sind. Wollen Sie zum Beispiel ein pair Objekt für eine Gehaltsliste (bestehend aus einem string für den Namen und einen float für das Gehalt) nach Namen vergleichen, so muss der Name als erstes angegeben werden. Wenn Sie dagegen die Gehälter vergleichen wollen, so sollten Sie das Gehalt als erstes angegeben. Die Reihenfolge ist aber nur dann relevant, wenn Sie die pair Objekte direkt miteinander vergleichen. Es bleibt Ihnen aber immer die Option offen, die Elemente first bzw. second eines pair direkt miteinander zu vergleichen.

Sie werden in Zukunft öfters Parameter mit den Bezeichnern rhs bzw lhs finden. rhs (right hand side) gibt den rechten Operanden des Operators an und lhs (left hand side) den linken Operanden.

Beispiel und Übung

Beispiel:

Das Beispiel implementiert eine Gehaltsliste in Form eines pair-Feldes. Da später die Gehälter und nicht die Namen verglichen werden sollen, enthält das pair als erstes Datum das Gehalt.

Damit die Handhabung von Objekten in pair Objekten ersichtlich wird, ist der Name innerhalb der Klasse Demo als string abgelegt. Diese Klasse Demo enthält alle notwendigen Memberfunktionen/Operatoren um ein Objekt dieser Klasse in einem pair verarbeiten zu können. Zusätzlich enthält die Klasse noch den überladenen << Operator für die Ausgabe des Namens. Beachten Sie auch die Abfrage am Anfang der Operator-Memberfunktion =.

In main() wird dann zunächst ein pair-Feld erstellt und initialisiert. Die Anzahl der Feldelemente wird mithilfe des sizeof(...) Operators berechnet.

Anschließend werden alle pair-Elemente ausgegeben.

Zum Schluss wird noch das Einkommen von zwei Feld-Elementen verglichen. Beachten Sie auch die 'trickreiche' Ausgabe des Vergleichs. Für den Vergleich wird der Bedingungsoperator ?: verwendet. Liefert die Auswertung des Vergleichsausdrucks true, so wird der String "weniger" an cout übergeben und im anderen Fall der String "mehr".

Pair-Array erstellen:
param-ctor
copy-ctor
dtor
param-ctor
copy-ctor
dtor
param-ctor
copy-ctor
dtor
Einkommen: 5000.00 Goldtaler     Name: Gustav Gans
Einkommen: 3000.00 Goldtaler     Name: Daisy Duck
Einkommen: 100000.00 Goldtaler   Name: Dagobert Duck
Gustav Gans verdient mehr als Daisy Duck
dtor
dtor
dtor


// Beispiel zu pair

#include <iostream>
#include <iomanip>
#include <utility>
#include <string>

using std::cout;
using std::endl;
using std::string;

// Minimale Demo Klasse für pair Anwendung
// ctor und dtor enthalten Ausgaben
class Demo
{
    string text;        // Nutzdatum
public:
    // ctors
    Demo()
    {
        cout << "std-ctor" << endl;
    }
    Demo(const char* pText): text(pText)
    {
        cout << "param-ctor" << endl;
    }
    Demo(const Demo& obj): text(obj.text)
    {
        cout << "copy-ctor" << endl;
    }
    // dtor
    ~Demo()
    {
        cout << "dtor" << endl;
    }
    // Zuweisungsoperator
    Demo& operator = (const Demo& lhs)
    {
        if (this == &lhs)         // Zuweisung auf sich selbst abfangen!
           return *this;
        text = lhs.text;
        cout << "= operator" << endl;
        return *this;
    }
    // Überladene Operatoren
    friend bool operator < (const Demo& rhs, const Demo& lhs);
    friend bool operator == (const Demo& rhs, const Demo& lhs);
    friend std::ostream& operator << (std::ostream& os, const Demo& obj);
};
// Kleiner als Operator
bool operator < (const Demo& lhs, const Demo& rhs)
{
    return lhs.text < rhs.text;   // string überlädt < Operator
}
// Gleich Operator
bool operator == (const Demo& rhs, const Demo& lhs)
{
    return rhs.text == lhs.text;  // string überlädt == Operator
}
// Shift left Operator für Ausgabe
std::ostream& operator << (std::ostream& os, const Demo& obj)
{
    cout << obj.text;
    return os;
}

// main() Funktion
int main()
{
    // pair-Array mit double/Demo-Objekten initialisieren
    cout << "Pair-Array erstellen:\n";
    std::pair<double,Demo> income[] = {
        std::make_pair(5000.00,Demo("Gustav Gans")),
        std::make_pair(3000.00,Demo("Daisy Duck")),
        std::make_pair(100000.00,Demo("Dagobert Duck"))};
    // Anzahl der Einträge im pair-Array
    const size_t SIZE = sizeof(income)/sizeof(income[0]);

    // Ausgabe auf 2 Dezimalstellen
    cout << std::fixed<< std::setprecision(2);

    // pair-Array ausgeben
    for (size_t index=0; index<SIZE; index++)
    {
        cout << "Einkommen: " << income[index].first << " Goldtaler";
        cout << "\t\tName: " << income[index].second << endl;
    }
    // Vergleich zweier Einkommen
    cout << income[0].second << " verdient ";
    cout << ((income[0] < income[1])? "weniger" : "mehr");
    cout << " als " << income[1].second << endl;
}

Übung:

Schreiben Sie ein Programm zum Abspeichern von Aktienkursen, wobei jeder Kurs aus einem Namen (als string-Datentyp) und dem Aktienwert (als float-Wert) besteht. Die Aktienkurse sind in einem entsprechenden pair-Feld abzulegen. Initialisieren das pair-Feld mit folgenden Werten:

Daimer         333.33f
VW             111.11f
BMW            222.22f
General Motors  11.11f

Das pair-Feld ist so anzulegen, dass die Aktienkurse auf einfache Weise verglichen werden können.

Geben Sie alle Aktienkurs aus.

Erhöhen Sie danach den Kurs der VW-Aktie um 111.11 und sortieren Sie die Aktienkurse aufsteigend nach ihrem Kurswert. Geben Sie zur Kontrolle nochmals alle Aktienkurse aus.

Aktienkurse:
-----------:
Hersteller: Daimler, Kurs: 333.33
Hersteller: VW, Kurs: 111.11
Hersteller: BMW, Kurs: 222.22
Hersteller: General Motors, Kurs: 11.11
** VW Aktie um 111.11 erhöht! **

Kurse nach Wert/Hersteller sortiert:
------------------------------------
Hersteller: General Motors, Kurs: 11.11
Hersteller: BMW, Kurs: 222.22
Hersteller: VW, Kurs: 222.22
Hersteller: Daimler, Kurs: 333.33

Lösung ansehen!