Kapitel 17 - Generische Programmierung

Peter Ulbrich

🚀 by Decker

Einleitung

  • Konzept der generischen Programmierung zielt auf Wiederverwendbarkeit ab
  • Umsetzung in C++ mit Templates (= Schablonen)
  • Ihr kennt sie schon!
    • std::vector,
    • std::unique_ptr,
  • Bisher: nur die Anwendersicht → „gewünschten Typ in Klammern angeben“
    • std::vector<int> vec; // Enthält nur int
  • Heute: „Öffnen der Motorhaube“ und Erstellen eigener Templates

Problemstellung

  • Aufgabe: Zwei Integer addieren
  • Aufgabe: Zwei Fließkommazahlen addieren
  • Aufgabe: Zwei 64-bit Werte addieren
  • Beobachtung: Kopie für jeden Datentyp
  • Problem
    • Aufwendig
    • Fehleranfällig
    • Wird unübersichtlich
// Zwei verschiedene Funktionen
// notwendig:
int add(int sum1, int sum2) {
    return (sum1 + sum2);
}

double add(double sum1, double sum2) {
    return (sum1 + sum2);
}

Die Lösung: Templates! 😀

Templates

  • Schlüsselwort template kündigt Schablone an
  • Abschnitt <typename T> beschreibt Schablonenparameter
  • T als Platzhalter für einen Datentyp
// Deklaration von Template-Funktionen
template <typename T>
T add(T sum1, T sum2) {
    return (sum1 + sum2);
}

// Mehrere verschiedene Typen auch ok
template <typename T, typename U>
T add_mixed(T sum1, U sum2) {
    return (sum1 + sum2);
}
  • Platzhalter ersetzt ein Datentyp bei Deklaration: T eine_variable
  • Konvention: Erster Buchstabe von Platzhaltern wird groß geschrieben

Beispiel - Template-Funktionen

#include <iostream>
using std::cout, std::endl;

// Deklaration von Template-Funktionen
template <typename T>
T add(T sum1, T sum2) {
    return (sum1 + sum2);
}
// Mehrere verschiedene Typen auch ok
template <typename T, typename U>
T add_mixed(T sum1, U sum2) {
    return (sum1 + sum2);
}

int main() {
    cout << "Add double: " << add<double>(2.5, 1.3) << endl;
    cout << "Add int: " << add<int>(4, 3) << endl;
    // Ggf. Rundungsfehler in Implementierung, abhängig von erstem Typ T
    cout << "Add_mixed int + double: " << add_mixed<int, double>(2.5, 1.3) << endl;
    cout << "Add_mixed double + int: " << add_mixed<double, int>(2.5, 1.3) << endl;

}
cpp

Terminologie

  • Automatische Umwandlung in C++ → implizite Template-Instanziierung

  • Umwandlung eines generischen Typs in einen Konkreten

    Monomorphisierung (Monomorphization)

  • Generische Funktionen / Klassen können neuen Programmcode erzeugen

    Meta-Funktionen / Meta-Klassen

    Metaprogrammierung

  • Häufiger Begriff: Template Metaprogramming (synonyme Verwendung)

    • Sehr beliebt, ein großer Teil der Sprache wird damit implementiert

Klassenschablonen

  • Auch Klassen können eine Schablone sein

  • Hier: Datentyp Pair

  • Zwei Templateparameter

    → Angabe zweier Datentypen

  • Randbemerkung: Gibt es auch in der STL

    std::pair verfügbar

// Einfaches Paaren zweier Klassen
template <typename T, typename U>
class Pair<T, U> {
public:
    T first;
    U second;
    void setFirst(T val) { first = val; }
    void setSecond(U val) { second = val; }
};

// Verwendung:
class Foo {};
class Bar {};

Pair<Foo, Bar> pair;
  • Ihr kennt Klassenschablonen schon! Aber woher? 🤔

Bspw. std::vector<int>

Beispiel - Klassenschablonen

#include <iostream>

using namespace std;

template <typename T, int NUM>
class Storage {
    T data[NUM];
    int pos;
public:
    Storage() : pos(0) {}
    void put(T val) {
        data[pos] = val;
        pos = (pos + 1) % NUM;
    }
    void print() {
        for (int i = 0; i < NUM; i++) {
            cout << data[i] << ",";
        }
    }
};

int main() {
    Storage<double, 3> a;

    a.put(3.2);
    a.put(1.42);
    a.put(0);
    a.print();
    return 0;
}
cpp

Templates

  • Während der Übersetzung: Compiler ersetzt Platzhalter

  • Compiler findet „unbekannten” Funktionsaufruf

    • „unbekannt” = Neuer Templateparameter
    • Kopieren des Template-Codes und Ersetzen der Platzhalter
    • Alle nachfolgenden Aufrufe verwenden dann die erstellte Kopie
  • Wichtig: Es wird für jeden Datentyp eine eigene Kopie erstellt

  • Ergebnis: Automatisch erzeugter Programmschnipsel

    • Ist auch nachher in Binärdatei so vorhanden
    • Für jeden Datentyp wird separater Code erzeugt

Fallstricke

  • Trennung von Programmcode in Header- und Source-Datei nicht möglich

    → Schablonen im Header definieren

  • Kann sehr unübersichtlich sein

  • Über Tricks ist die Trennung dennoch möglich

    • Deklarierung des Templates im Header, Definition des Template-Codes in separater Datei (wie gehabt)
    • Am Ende des Headers #include-Anweisung: Fügt Quellcode in den Header ein

Automatische Ableitung von Parametern

  • Compilter muss Templateparameter kennen
  • Problem: Eine neue Instanz erzeugen?
  • Folge: Compiler-Fehler, wenn Doppeldeutigkeiten vorhanden
  • Compiler kann Parameter automatisch ableiten
#include <iostream>

using namespace std;

template <typename T, typename U>
struct Pair {
    T first;
    U second;
    Pair(T f, U u) : first(f), second(u) {}
};

int main() {
    // 0 Typen angegeben. ERROR
    Pair<> p1 = {1, 2};
    cout << p1.first << endl;

    Pair<int, int> p2 = {1, 2}; // Ok
    cout << p2.first << endl;

    // Ab C++20 ok (siehe Regelliste)
    Pair p3 = {3.5f, 2.3f};
    cout << p3.first << endl;

    Pair<float, float> p4 = {3.5f, 2.3f}; // Ok
    cout << p4.first << endl;
}
cpp
  • Empfehlung: Typen immer explizit angeben

Beispiel - Ableiten von Templateparametern

#include <iostream>
using std::cout, std::endl;

template <typename T>
T add(T sum1, T sum2) {
    return (sum1 + sum2);
}

int main() {
    cout << "Add double: " << add(2.5, 1.3) << endl;
    cout << "Add int: " << add(4, 3) << endl;
}
cpp

Standardwerte für Schablonenparameter

  • Schablonenparameter können Standardwerte haben
  • Instanziierung erfolgt mit diesen, ohne explizite Angabe
template <typename T = int,
    typename U = double>
struct Pair {
    T first;
    U second;
};

int main() {
    Pair p1; // <int, double>
}

Herausforderungen mit Schablonen

  • Sind nützlicher Teil von C++
  • Wichtig: Beachtet einige Punkte
    • Viele Templates → erhöhte Übersetzungszeit (Instanziierung)

    • Kann auch unnötiger Code generiert werden → größeres Binary

    • Fehler in (geschachtelter) Templateprogrammierung sind anspruchsvoll

      → Kryptische Fehlermeldungen beim Kompilieren

  • Aber: Eher kleiner Nachteil für flexiblen Programmcode

Typbeschränkung

  • Wunsch: Vorgaben an Schablonenparameter
    • Beispiel: Addieren zweier Objekte Foo und Bar mithilfe von add()
    → wenig sinnvoll, sollte nicht erlaubt sein
  • Dank C++20: Umsetzung mittels Constraints möglich
    • Definieren Beschränkungen (→ Name) für einen Platzhalter
    • Ansonsten: Abbruch während des Kompilierens
  • requires markiert Einschränkung
// add() soll nur Integer bzw.
// float/double akzeptieren
#include <type_traits>

template <typename T>
// T entweder Integer ODER Float
requires std::is_integral_v<T> ||
    std::is_floating_point_v<T>
T add(T sum1, T sum2) {
    return sum1 + sum2;
}

Aufbau von Typbeschränkungen

  • Verknüpfung mehrerer Eigenschaften mit && (logisches UND) bzw. || (logisches ODER)
  • Wie bei Mengenlehre: Wird kompiliert, wenn alle Eigenschaften oder eine oder mehr der Eigenschaften erfüllt werden
  • Prüffunktionen geben dabei nur true oder false zurück
// add() soll nur Integer bzw.
// float/double akzeptieren
#include <type_traits>

template <typename T>
// T entweder Integer ODER Float
requires std::is_integral_v<T> ||
    std::is_floating_point_v<T>
T add(T sum1, T sum2) {
    return sum1 + sum2;
}

Beispiel - Constraints

// add() soll nur Integer bzw.
// float/double akzeptieren
#include <iostream>
#include <type_traits> // für is_integral_v bzw. is_floating_point_v

using std::cout, std::endl;

template <typename T>
// Entweder Integer ODER Float
requires std::is_integral_v<T> || std::is_floating_point_v<T>
T add(T sum1, T sum2) {
    return sum1 + sum2;
}

class Foo {};
int main() {
    cout << add<int>(3,4) << endl;
    cout << add<float>(2.5, 1.3) << endl;
    // Error: constraints not satisfied
    // cout << add<Foo>(Foo(), Foo()) << endl;
}
cpp

Concepts

  • Zusammenfassen von Constraints

    Concept

  • Bildung eines abstrakten Typen, der alle Eigenschaften erfüllt

  • Erspart Schreibarbeit und erlaubt einfache Wiederverwendung

#include <type_traits>

// Jetzt als abstrakter Typ
template <typename T>
concept FloatOrInt = std::is_integral_v<T>
    || std::is_floating_point_v<T>;

template <typename T>
// Benutzung identisch
requires FloatOrInt<T>
T add(T sum1, T sum2) {
    return sum1 + sum2;
}
  • Analog zur objektorientierten Programmierung werden jetzt verschiedene Eigenschaften zu einem gemeinsamen Typ zusammengefasst

Beispiel - Concepts

#include <iostream>
#include <type_traits>

using std::cout, std::endl;

// Jetzt als abstrakter Typ
template <typename T>
concept FloatOrInt = std::is_integral_v<T> || std::is_floating_point_v<T>;

template <typename T>
requires FloatOrInt<T> // Benutzung identisch
T add(T sum1, T sum2) {
    return sum1 + sum2;
}

class Foo {};
int main() {
    cout << add<int>(3,4) << endl;
    cout << add<float>(2.5, 1.3) << endl;
    // Error: constraints not satisfied
    // cout << add<Foo>(Foo(), Foo()) << endl;
}
cpp

Typeinschränkung in der STL

  • Standardbibliothek bietet eine Reihe von vordefinierten Constraints und Concepts
  • Beispiel: „Klasse X ist abgeleitet von Y“
    • std::derived_from<X,Y>; // Teil des Headers <concepts>
  • Für vollständige Liste vordefinierter Constraints und Concepts siehe Dokumentation

Resümee Typeneinschränkung

  • Fazit: Template-Metaprogramming ist ein zweischneidiges Schwert
  • Auf der einen Seite: Sehr mächtiger Werkzeugkasten
  • Auf der anderen Seite: Wird sehr schnell sehr komplex
  • Art und Umfang der Templateprogrammierung hängt vom Projekt ab. Es ist auf jeden Fall sehr nützlich, sie zu kennen.
  • Empfehlung: Fangt klein an! Nehmt kleine Beispiele

Exkurs: Embedded Template Library

  • Großes Problem: STL auf eingeschränkten Geräten häufig nicht lauffähig
  • Simpler Grund: Keine dynamische Speicherallokierung erlaubt
  • Betrifft zum Beispiel bereits kennengelernte Speichervergrößerung bei std::vector (Verdoppelung des alten Werts)
  • Zwei Auswege
    1. Erstellen einer eigenen Allokator-Funktion
    2. Verwenden einer externen Bibliothek: Embedded Template Library (ETL)
  • Nicht Teil des C++-Standards, reimplementiert aber die Algorithmen und Datenstrukturen mit statischem Speicher
  • Sehr nützlich, diese zu kennen! 😀

Exkurs: Statische Polymorphie

  • Problem: Virtuelle Methoden werden dynamisch gebunden
    • vtable kostet Speicher, Rechenzeit
    • Auf Eingebetteten Systemen ggf. nicht erwünscht
  • Statische Polymorphie: Basisklasse als Template
    • Ruft abgeleitete Klasse auf
    • static_cast wählt statisch die Kind-Implementierung
    • Keine vtable und kein dynamisches Binden mehr
#include <iostream>

template <typename T>
struct Shape {
    void draw() const {
        static_cast<const T&>(*this).drawImpl();
    }
};

struct Circle : Shape<Circle> {
    void drawImpl() const { 
    	std::cout << "Circle\n"; 
    }
};

struct Square : Shape<Square> {
    void drawImpl() const { 
    	std::cout << "Square\n"; 
    }
};

int main() {
    Circle c;
    Square s;

    c.draw(); 
    s.draw(); 
}
cpp

Zusammenfassung

  • Schablonen für Funktionen und Klassen
  • Gut: Erlauben wiederverwendbaren Code
    • Einmal schreiben
    • Für verschiedene Datentypen nutzen
    • Reduziert Fehleranfälligkeit
  • Aber: Schablonen werden schnell komplex
    • Fehlermeldung teils schwer zu verstehen
Logo Sys
Kapitel 17 - Generische Programmierung