JGU Logo JGU Logo JGU Logo JGU Logo

Institut für Informatik

Michael Wand
David Hartmann
Sommersemester 2020DIGITAL

Weitere Architekturbeispiele & Lambda-Ausdrücke

Übung 8
Einführung in die Softwareentwicklung







Aufgabe Stream-Wrapper

Letzte Änderung: 15. June 2020, 13:54 Uhr
20 Punkteim Detail
Ansicht:   |  

Änderungshistorie:



Interface:
Als Basis definieren wir die abstrakte Klasse Stream, die das Interface eines Streams festlegt: Jeder Stream stellt eine Methode print bereit, mit der Daten in den Stream geschrieben werden können. Wohin die Daten dann tatsächlich geschrieben werden, ist durch dieses Interface noch nicht festgelegt (theoretisch darf ein Stream auch Daten wegwerfen).

class Stream {
public:
    virtual void print(const std::string&) = 0;
    virtual ~Stream() = default;
};

Wrapper für std::ostream:
Wir nutzen nun das Interface Stream, um eine konkrete Implementierung eines Streams vorzugeben. Die unterhalb definierte Klasse BasicStreamWrapper gibt den übergebenen String auf einem C++-kompatiblen Output-Stream (etwa std::cout) aus und wrappt damit wie die Verwendung von C++-Streams. (Eine mögliche Verwendung wird weiter unten in einem Beispielcodeschnipsel vorgeführt).

BasicStreamWrapper.h

// .h
class BasicStreamWrapper : public Stream {
private:
    std::ostream& stream;
public:
    explicit BasicStreamWrapper(std::ostream& s): stream(s) {};
    void print(const std::string&) override;
    ~BasicStreamWrapper() override = default;
};

BasicStreamWrapper.cpp

// .cpp
void BasicStreamWrapper::print(const std::string& string) {
    stream << string;
}

Dekoratoren: Wir können jetzt Modifikationen implementieren, die den auszugebenen String mit Zeilennummern versehen (CountDecorator), ein Trennzeichen hinten anfügen (PostfixDecorator) oder sogar den eigentlichen Inhalt des Strings überschreiben (CaesarDecorator). Um solche Modifikatoren nach einem Baukasten-System zusammensetzen zu können, bedienen wir uns des Decorator-Patterns: Wir leiten für jede Modifikation einen weiteren Stream ab, der in seiner print-Methode die gewünschten Änderungen am String durchführt, dann aber das Ergebnis nicht sofort ausgibt, sondern an einen anderen Stream weitergibt. Der "Dekorator"-Stream hat also dasselbe Interface wie ein normaler Stream, verhält sich aber anders.

class StreamDecorator : public Stream {
protected:
    Stream* stream;
public:
    explicit StreamDecorator(Stream* s) : stream(s) {};
    ~StreamDecorator() override = default;
};

Die Klasse StreamDecorator ist unser Ausgangspunkt für diese Implementierung. StreamDecorator leitet zwar von Stream ab, überschreibt aber nicht die pure virtuelle Methode print und ist daher selbst eine abstrakte Klasse, kann also nicht instanziiert werden. Jeder StreamDecorator führt einen Zeiger auf den Stream mit, auf den letztendlich geschrieben werden soll.


Beispielverwendung:

Wenn Sie alles implementiert haben, sollte das folgende Codebeispiel funktionieren:

#include <ostream>
#include <string>

// Ihre selbst definierten Header kommen hierher.

void do_something(Stream &stream) {
    stream.print("Hello World");
    stream.print("Second Line");
}

int main() {
    std::ostream& out = std::cout;

    BasicStreamWrapper cout(out);
    PostfixDecorator postfix(&cout, ".\n");
    CountDecorator count(&postfix);
    CaesarDecorator caesar(&count, 7);
    do_something(count);
    do_something(caesar);
}

Die Ausgabe ist dann

[0] Hello World.
[1] Second Line.
[2] Olssv Dvysk.
[3] Zljvuk Spul.

Aufgaben

  1. Wir implementierun nun drei spezialisierte Decorators, die von StreamDecorator erben:
    1. Implementieren Sie als erstes den Wrapper CountDecorator, der vor jede Ausgabe eine automatisch inkrementierende Ausgabennummer anhängt.
      class CountDecorator : public StreamDecorator {
          uint64_t counter;
      public:
          explicit CountDecorator(Stream* s, uint64_t initial_counter = 0);
          void print(const std::string&) override;
          ~CountDecorator() override = default;
      };
      


      Um Ihnen den Einstieg zu erleichtern, geben wir die Implementierung des Konstruktors von CountDecorator vor. Die Syntax ... : StreamDecorator(s) ... ruft den Konstruktor der Basisklasse StreamDecorator mit dem gewrappten Stream auf.

      CountDecorator::CountDecorator(Stream *s, uint64_t initial_counter) : StreamDecorator(s), counter(initial_counter) {}
      
    2. Implementieren Sie einen PostfixDecorator. Dieser soll einen vorher definierten String an jeden String anhängen, der print übergeben wird.
      class PostfixDecorator : public StreamDecorator {
      private:
          std::string postfix_string;
      public:
          explicit PostfixDecorator(Stream* s, const std::string& postfix_string = "\n");;
          void print(const std::string&) override;
          ~PostfixDecorator() override = default;
      };
      
    3. Implementieren Sie einen CaesarDecorator. Ein CaesarDecorator soll jeden String, der an print übergeben wird, nach dem Caesar-Chiffre verschlüsseln. Das Caesar-Chiffre ist eine sehr einfache Verschlüsselung, bei der alle Buchstaben einfach verschoben werden. Andere Zeichen bleiben unverändert. Die Zeichen der oberen Reihe werden in das jeweilige Zeichen der unteren Reihe übersetzt. Hier ein Beispiel für eine Verschiebung um 5 Buchstaben:
      Klar:    a b c d e f g h i j k l m n o p q r s t u v w x y z
      Geheim:  f g h i j k l m n o p q r s t u v w x y z a b c d e
      
      Die Buchstabenverschiebung wird Ihrer Klasse als Konstruktorparameter übergeben.

      class CaesarDecorator : public StreamDecorator {
      private:
          int32_t shift;
      public:
          explicit CaesarDecorator(Stream* s, int32_t shift = 0);
          void print(const std::string&) override;
          ~CaesarDecorator() override = default;
      };
      
  2. Finden Sie ein Beispiel für zwei Dekoratoren, bei denen die Reihenfolge, in der sie geschachtelt werden, eine Rolle spielt.
  3. Da StreamDecorator eine beliebige Klasse vom Typ Stream erwartet können wir auch andere Basisobjekte vom Typ Stream dekorieren ohne unseren Klassen vom Typ StreamDecorator umschreiben zu müssen. Wir tun dies, indem wir eine Klasse StreamPrintWrapper definieren, die statt einem std::ostream die Methode printf (Link zur Dokumentation) wrappt. Implementieren Sie also class StreamPrintWrapper : public Stream { ... } als zweite Basisklasse.
  4. Zeichnen Sie ein UML-Diagramm, das die sechs hier vorgestellten Klassen in Beziehung setzt.
  5. Eine Verschlüsselungsmethode (insbesondere eine so einfache wie die Caesar-Verschlüsselung, die Sie bitte niemals für irgend etwas außer für Lernzwecke verwenden ;)), sollte es niemals erlauben, Wissen über den Inhalt einer Nachricht aus ihrer verschlüsselten Version preiszugeben. Wenn wir jedoch ein ein Postfix-Wort anhängen, könnten wir folgern, dass ein Wort stets auf die selbe Verschlüsselung abgebildet wird, was einem Cracken dieser Methode bereits eine große Hilfestellung wäre.1.
    Selbst wenn wir die Kryptographie außer acht lassen, ist es doch schade, dass das Postfix-Wort auch stets mit verschlüsselt werden kann.

    Aus diesen beiden Gründen möchten wir ermitteln, wie man dem Compiler in unserem Framework erklären kann, dass eine bestimmte Reihenfolge von Dekoratoren einzuhalten ist. Dazu definieren wir
    • BasicStream als Interface, das zwischen den Basisklassen (also StreamPrintWrapper und BasicStreamWrapper) und Stream sitzt. Die beiden Basisklassen implementieren nun BasicStream statt Stream
      class BasicStream : public Stream {
      public:
          virtual void print(const std::string&) = 0;
          virtual ~BasicStream() = default;
      };
      
    • die Klasse StreamDecoratorLevelOne so, dass zwar einen allgemeinen Stream* dekoriert, aber nicht mit beliebigen Streams konstruiert werden darf, sondern nur anderen StreamDecoratorLevelOne (also der selbe Typ wie der Decorator selbst) oder anderen Basisklassen (BasicStream):
      class StreamDecoratorLevelOne : public Stream {
      protected:
          Stream* stream;
      public:
          // man beachte hier die zwei Konstruktoren, die keine beliebigen Stream-Objekte zulassen, aber weiterhin beliebige Stream* speichern.
          explicit StreamDecoratorLevelOne(StreamDecoratorLevelOne* s) : stream(s) {};
          explicit StreamDecoratorLevelOne(BasicStream* s) : stream(s) {};
          ~StreamDecoratorLevelOne() override = default;
      };
      


    Wir (als Programmier) können durch eine solche Stellung in der Klassenhierarchie dafür sorgen, dass eine gewisse Reihenfolge beim Zusammensetzen einer Klasse eingehalten werden muss, mit anderen Worten, dass die Logik der Kapselung durch den Compiler geprüft wird!

    Ändern Sie die Klassen CaesarDecorator, PostfixDecorator und CountDecorator so ab, dass die Verslüsselung stets nur den eigentlich auszugebenden Text verschlüsselt, nicht aber eventuelle Postfixes oder Prefixes (wie beim CountDecorator).
    Es soll also nicht möglich sein, PostfixDecorator(CaesarDecorator(Stream *)) aufzurufen.
  6. Bonus: Passen Sie das UML-Diagramm so an, dass auch die neuen Klassen aus Aufgabenteil 5 enthalten sind.
  7. Bonus: Implementieren Sie einen Dekorator für das Caesar-Chiffre, der die Verschlüsselung wieder umkehrt.
  8. Bonus: Implementieren Sie einen weiteren Dekorator Ihrer Wahl.




Aufgabe Higher-Order Functions

Letzte Änderung: 22. June 2020, 10:48 Uhr
20 Punkteim Detail
Ansicht:   |  

Änderungen:



In der Vorlesung haben wir das Konzept der funktionalen Programmierung kennen gelernt. Funktionszeiger sind eine Möglichkeit, Funktionen an andere Funktionen als Argument zu übergeben. In vielen Fällen ist es jedoch unnötig globale Funktionen zu definieren, die dann per Funktionszeiger an etwa nur einer Stelle eingebunden werden. An dieser Stelle kommen Lambda-Funktionen ins Spiel, die Sie eventuell bereits aus Python kennen.


Lambda-Funktionen in C++

In C++ sehen Lambda-Funktionen folgendermaßen aus:

    [](int a, int b) -> int {return a+b;}

Dabei bezeichnet [] die „Capture-List“, () die Argumentliste, der Teil nach -> den Rückgabetypen und {} umfasst den Funktionsrumpf.


Alle Variablen in der Capture-List werden aus dem umgebenden Scope "eingefangen" und stehen der Lambda-Funktion je nach Deklaration als Kopie oder per Referenz zur Verfügung. Beispielsweise für die Variable a wird die Variable mittels

übergeben. Es ist auch möglich, mehrere Variablen anzugeben, etwa durch [x, &y]. Genauso können alle Variablen der umgebenden Scopes eingefangen werden, und zwar durch


Hier gilt außerdem:


Beispiel:

    auto sum = [](int32_t a, int32_t b){return a + b;}
    int32_t c = sum(5, 6);
    int32_t d = sum(4, 8);

Higher-Order-Functions in C++

Lambdas in Variablen zu speichern und dann damit zu arbeiten ist noch nicht sehr spannend. Interessant wird es aber vor allem dann, wenn wir Funktionen definieren wollen, die selbst wieder Funktionen generieren und/oder Funktionen als Argument(e) erhalten, Higher-Order-Functions (siehe Vorlesung).


Beispiel:

    #include <cmath>
    #include <iostream>
    #include <functional> // benötigt für den abstrakten Typ std::function

    // Diese Funktion wendet f auf das größere der beiden int32_t-Argumente an.
    // Dabei bildet f von einem int32_t auf ein double ab, was in Code nur implizit steht.
    // std::function<double(int)> steht hierbei für eine Funktion, die ein int auf ein double Abbildet. 
    // (In der Klammer können auch mehrere durch Kommata getrennte Argumente stehen).
    double transform_max(int a, int b, std::function<double(int)> f) {
        return a > b ? f(a) : f(b);
    }

    // Alternativ können Lambdas auch mithilfe von Templates einer Funktion übergebenen werden
    // Das Ganze sieht dann folgendermaßen aus:
    template <typename Func>
    double transform_max_tl(int32_t a, int32_t b, Func f) {
        return a > b ? f(a) : f(b);
    }

    // Diese Funktion generiert einne Funktion, die prüft, ob eine Zahl < max ist.
    // bool ist hierbei die Ausgabe aus der Eingabe int
    std::function<bool(int)> make_less_predicate(int max) {
        return [max](int val) -> bool {
            return val < max;
        };
    }

    auto make_less_predicate_tl(int32_t max) {
        return [max](int32_t val) -> bool {
            return val < max;
        };
    }

    int main() {

        // Lambdas können auch direkt übergeben werden, ohne erst in einer Variable
        // gespeichert zu werden. (Insbesondere, wenn man sie nur einmal verwendet)
        double t = transform_max(89, 123, [](int32_t c) -> double {
            return std::sqrt(c);
        });

        // Hinweis: Bei der templatisierten Version wird das Templateargument automatisch ermittelt.
        double t2 = transform_max_tl(89, 123, [](int32_t c) -> double {
            return std::sqrt(c);
        });

        auto less42 = make_less_predicate(42);
        auto less5 = make_less_predicate(5);
        std::cout << "Test: " << less5(8) << " " << less5(48) << std::endl;
        std::cout << "Test: " << less42(8) << " " << less42(48) << std::endl;
    }

Übrigens: Ein Prädikat ist dabei im Allgemeinen der Teil einer atomaren Aussage, der wahrheitsfunktional ist (Quelle: Wikipedia). In unseren Beispielen sind dies also Funktionen, die für beliebige Eingabe entweder wahr (true) bzw falsch (false) zurückgeben.

Aufgaben

Implementieren Sie die folgenden Higher-Order Functions und zeigen Sie ihre Verwendung an mindestens einem Beispiel.

  1. void for_each(std::vector<float> &v, std::function<void(float)> f)
    Führt die übergebenen Lambda-Funktion vom Typ void(float) für jedes Element aus.
    (Dieses kann zum Beispiel jedes Element auf der Konsole ausgeben).
  2. float find_if(std::vector<float> &v, std::function<bool(float)> f)
    Gibt das erste Element der übergebenen Liste aus, das das Prädikat (Funktion vom Typ bool(float)) erfüllt.
  3. float count_if(std::vector<float> &v, std::function<bool(float)> f)
    Zählt die Elemente, die das Prädikat (Funktion vom Typ bool(float)) erfüllen.
  4. void transform(std::vector<float> &v, std::function<float(float)> f)
    Überschreibt jedes Elment der übergebenen Liste mit dem durch die Funktion f (Typ float(float)) transformierten Element.
  5. float accumulate(std::vector<float> &v, std::function<float(float,float)> f, float start = 0.0)
    Summiert die Werte der Liste mithilfe der übergebenen Funktion (Typ float(float,float)) auf.

    Es gilt also accumulate(v,f,0) = f(...(f(f(start,v[0]),v[1]),...),v[n]).
  6. void partition(std::vector<float> &v, std::function<bool(float)> f)
    „Sortiert“ alle Elemente, die das Prädikat (Typ bool(float)) erfüllen an den Anfang der Liste und alle, die das Prädikat nicht erfüllen ans Ende der Liste.
    (Hierfür ist kein Sortieralgorithmus nötig — einfach über die Liste iterieren reicht vollkommen).
  7. template <typename Func1, typename Func2> auto compose(Func1 f1, Func2 f2)
    Erhält zwei Funktionen f1 und f2 und gibt die Funktion zurück, die das Ergebnis von f2 in f1 einsetzt.
    Beide Funktionen haben den Typ float(float);
    Mathematisch ausgedrückt suchen wir die Funktion \[ f_1 \circ f_2: x \mapsto f_1(f_2(x)) \]

    (Hinweis: Wir möchten an dieser Stelle die Verwendung templatisierten Funktionen höherer Ordnung üben.)
  8. template <typename Func> auto partial(Func f, float val)
    Erhält eine Funktionen f1 mit zwei Argumenten (z.B. Typ float(float,float)) und soll eine Funktion zurückgeben, die das zweite Argument von f festsetzt.
    Diese Funktion hat dann den Typ float(float). Mathematisch ausgedrückt suchen wir die Funktion \[ f_a: x \mapsto f(x,a). \]

    (Hinweis: Wir möchten an dieser Stelle die Verwendung templatisierten Funktionen höherer Ordnung üben.)
  9. Bonus: Ganz ähnlich wie find_if, count_if könnten auch Funktionen wie etwa any_of, all_of, none_of implementiert werden. Diese sollen die Frage beantworten, ob ein Prädikat für irgendein/kein/alle Elemente gilt.
  10. Bonus: Ganz ähnlich zu partition kann auch ein sort implementiert werden, dass basierend auf einem Comparator-Prädikat vom Typ bool(float,float) (also zwei Eingaben) die Liste sortiert — true entspricht dabei der Aussage, dass die linke Eingabe kleiner ist als die zweite Eingabe.




Aufgabe Bonusaufgabe: Projekterweiterung

Letzte Änderung: 08. June 2020, 14:40 Uhr
zusätzlich 20 Punkteim Detail
Ansicht:   |  


Implementieren Sie die Möglickeit weiter Spielfelder mit einander zu verbinden.
Alle Spielfelder sollen durch „Tunnel“ mit einander verbunden werden; bewegt sich ein Objekt in den Tunnel, soll es ganz einfach von einem Spielfeld auf das andere Gesetzt werden, das der Tunnel verbunden hat.
Wichtig Erweitern Sie den Code durch eine Tunnel-Klasse! Sinn dieser Aufgabe ist es, beliebige viele Tunnel einzubinden zu können.


Beispiel: Bewegungssimulation
┌─┬─┬─┬─┬─┬─┬─┬─┐
│ │ │♘│ │ │ │ │ │
├─┼─┼─┼─┼─┼─┼─┼─┼─┬─┐
│ │ │ │ │ │ │ │ │ │ │ tunnel
├─┼─┼─┼─┼─┼─┼─┼─┼─┴─┘
│ │ │ │ │ │ │ │ │
├─┼─┼─┼─┼─┼─┼─┼─┤
│ │ │ │ │ │ │ │♞│
├─┼─┼─┼─┼─┼─┼─┼─┤
│ │ │ │ │ │ │ │♚│
├─┼─┼─┼─┼─┼─┼─┼─┤     ┌─┬─┬─┬─┬─┬─┬─┬─┐
│ │♔│ │ │ │ │ │ │     │ │ │ │ │ │ │ │ │
├─┼─┼─┼─┼─┼─┼─┼─┤     ├─┼─┼─┼─┼─┼─┼─┼─┤
│ │ │ │ │ │ │ │ │     │ │ │ │ │ │ │♚│ │
├─┼─┼─┼─┼─┼─┼─┼─┤     ├─┼─┼─┼─┼─┼─┼─┼─┤
│♘│ │ │ │ │♞│ │ │     │ │ │ │ │ │ │ │ │
└─┴─┴─┴─┴─┴─┴─┴─┘     ├─┼─┼─┼─┼─┼─┼─┼─┤
                      │ │ │♘│ │ │ │ │ │
                      ├─┼─┼─┼─┼─┼─┼─┼─┤
                      │ │ │ │ │ │ │ │ │
                  ┌─┬─┼─┼─┼─┼─┼─┼─┼─┼─┤
           tunnel │ │ │ │ │ │ │ │ │ │ │
                  └─┴─┴─┴─┴─┴─┴─┴─┴─┴─┘






Beispiel: Verkehrssimulation
                      +---------------------------------------+
                      | v<<<.<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<< |
                      | v --^ ---  --  --  --  --  --  -- - ^ |
                      | .>>>.>>>>>>>>>>>>>>>>>>>>>>>>>>>v   ^ |
                      | v | ^ +-----------------------+ v | ^ |
                      | v   ^ |                       | v   ^ |
                      | v | ^ |                       | v | ^ |
                      | v   ^ |                       | v   ^ |
                      | v | ^ |                       | v | ^ |
                      | v   ^ +-----------------------+ v   ^ |
                      | v   ^<<<<<<<<<<<<<<<<<<<<<<<<<<<.<<<. |
                      | v--  ---  --  --  --  --  --  - v T ^ |
                      | >>>>>>>>>>>>>>>>>>.>>>.>>>>>>>>>>>>>^ |
                      +-----------------| v   ^ |-------------+
                                        | v | ^ |
                                        | v   ^ |
                                        | v | ^ |
                                        |_-----_|
                                        /       \
                                         tunnel



+------------------------------------------------------------------------------------+
| v<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<< |
| v  --  --  --  --  --  --  --  --  --  --  --  --  ---  --  --  --  --  --  -- - ^ |
| v | >>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>.>>>.>>>>>>>>>>>>>>>>>>>>>>>>>>>v   ^ |
| v   ^ +------------------------------------+ v | ^ +-----------------------+ v | ^ |
| v | ^ |                                    | v   ^ |                       | v   ^ |
| v   ^ |                                    | v | ^ |                       | v | ^ |
| v | ^ |                                    | v   ^ |                       | v   ^ |
| v   ^ |                                    | v | ^ |                       | v | ^ |
| v | ^ |              +---------------------+ v   ^ +-----------------------+ v   ^ |
| v   ^ |              | v<<<<<<<<<<<<<<<<<<<<<<<<<.<<<<<<<<<<<<<<<<<<<<<<<<<<<.<<<^ |
| v | ^ |              | v  --  --  --  --  --  --  ---  --  --  --  --  --  - v T ^ |
| v   ^ |              | v | >>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>.>>>^ |
| v | ^ |              | v   ^ +---------------------------------------------+ v   ^ |
| v   ^ |              | v | ^ |              tunnel                         | v | ^ |
| v | ^ +--------------+ v   ^ |             _________                       | v   ^ |
| v   ^<<<<<<<<<<<<<<<<<<.<<<. |            | _-----_ |                      | v | ^ |
| v | ^  --  --  --  --  v   ^ |            |/ v   ^ \|                      | v   ^ |
| .>>>.>>>>>>>>>>>>>>>>>>.>>>^ |             | v | ^ |                       | v | ^ |
| v | ^ +--------------+ v   ^ |             | v   ^ |                       | v   ^ |
| v   ^ |              | v | ^ |             | v | ^ |                       | v | ^ |
| v | ^ |              | v   ^ |             | v   ^ |                       | v   ^ |
| v   ^ +--------------+ v | ^ +-------------+ v | ^ +-----------------------+ v | ^ |
| v | ^<<<<<<<<<<<<<<<<<<.<<<.<<<<<<<<<<<<<<<<<<<<<.<<<<<<<<<<<<<<<<<<<<<<<<<<<<   ^ |
| v  --  --  --  --  --  v T ^   --  --  --  --  --  ---  -- ^--  --  --  --  -- - ^ |
| >>>>>>>>>>>>>>>>>>>>>>>.>>>.>>>>>>>>>>>>>>>>>>>>>>>>>>>.>>>.>>>>>>>>>>>>>>>>>>>>>^ |
+----------------------+ v | ^ +-----------------------+ v   ^ +---------------------+
                       | v   ^ |                       | v | ^ |
                       | v | ^ |                       | v   ^ |
                       | v   ^ |                       | v | ^ |
                       | v | ^ |                       | v   ^ |
                       | v   ^ +-----------------------+ v | ^ |
                       | v | ^<<<<<<<<<<<<<<<<<<<<<<<<<<<<   ^ |
                       | v  --  --  --  --  --  --  -- --  - ^ |
                       | >>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>^ |
                       +---------------------------------------+




[1] Caesar zu cracken ist eh nicht wirklich schwierig.