C++Guns – RoboBlog blogging the bot

22.03.2017

C++ Guns - Funktionale Anwendung erkannt

Filed under: Allgemein — Tags: — Thomas @ 15:03

Ich habe gerade eine einfache Schleife mit 5 Zeilen geschrieben und auf einmal sah ich, dass das doch ein gutes Beispiel für funktionales programmieren wäre.
Gegeben ist ein Datenvector mit custom Typ und gesucht ist ein bestimmtes Datum und, falls vorhanden, davon ein bestimmter Wert. Die Schleife lässt sich von Hand so ausdrücken:


    struct Blah {
        size_t idx;
        int att;
    };

    vector<Blah> data { {2, -1}, {1, -1}, {5, 50}, {3, 10}};

    // skip -1
    size_t start = 0;
    for(size_t i=0; i < data.size(); ++i) {
        int att = data[i].att;
        if(att != -1) {
            start = data[i].idx;
            break;
        }
    }
    cout << "start " << start << "\n"; // print 5

Hier gibt es gleich eine ganze Reihe von Fehlerquellen. Von welchem Typ muss i sein, size_t oder int? Inkrement mit ++i oder i++ ? Ist der operator[] überhaupt vorhanden, und wenn ja auch effizient? Muss der Zugriff ueber const Referenz erfolgen? Und die Schleifen koennen wir beenden beim ersten Treffer, ja sicher oder ist das "break" zufällig da. Fuer alle das, muss man den Code Zeile fuer Zeile lesen und nachvollziehen und verstehen. Wie immer ist das "WAS und nicht WIE" Prinzip verletzte. Denn der Code sagt nicht, WAS er macht, sondern nur WIE. Ich hätte auch range-for nehmen können. Aber das ändert am Prinzip nichts.

Aus funktionaler Sicht ist die Schleife ja ein Muster fuer "find_if_apply" oder "apply_if_find" je nachdem ^^ Ob es in der funktionalen Welt so eine Funktion gibt, und wie sie genau heißt, weiß ich nicht. Aber ich probier mal meine eigene Version. Ich wäle den Namen "find_if_apply", da es schon eine C++ Funktion std::find_if gibt, und erweitere sie um eine "apply" componente.


template<class Container, class UnaryPredicate, class UnaryFunction>
void find_if_apply(const Container& data, UnaryPredicate p, UnaryFunction f) {
    for(const auto& x : data) {
        if(p(x)) {
            f(x);
            break;
        }
    }
}

size_t start = 0;
find_if_apply(data,
              [](const Blah& x) { return x.att != -1; },
              [&start](const Blah& x) { start = x.idx; }
              );
cout << "start " << start << "\n"; // print 5

Die Funktion find_if_apply nimmt zwei den Datensatz und zwei Funktionen. Die erste ist fuer find, und die zweite fuer apply.
Also die template Funktion selbst ist wirklich überraschend erstauling elegant einfach. Die Anwendung davon ist auch okay. Zwei einzeiler Funktionen sind noch lesbar.

Übrigends, std::find_if alleine hätte nichts geholfen. Da ein Iterator zurueck gegeben wird der wieder den Scope voll müllt und manuell auf Gültigkeit geprüft werden muss. Mit std::find_if wäre die Schleife und das if() weg gekapselt, aber gebracht hätte es nichts.

Noch eine Bemerkung: Die Variable idx in dem Beispiel ist explizit angegeben. Überwiegend kommt das in unseren Datenstrukturen und Algorithmen aber nicht vor. Dort ist die Position im Array der Index. Und schon funktioniert der funktionale Ansatz wieder nicht. Man könnte find_if_apply natürlich so erstellen und dokumentieren, dass das erste Argument der Lambdas der Laufindex ist. Aber nicht immer wird er auch in den Lambdas benötigt. Da mit Overloads und sonstige Tricks kommen macht alles wieder nur viel zu kompliziert. Noch dazu hat nicht jeder Container auch ein Laufindex. Bei Listen funktioniert es ist nicht, und da ist es dann Datenstruktur abhängig, wo der Index des Datums steht.

21.03.2017

numpad game

Filed under: Allgemein — Tags: , — Thomas @ 09:03

Wer ist schneller beim Zahlen eintippen über den Nummerntastenfeld?
Zwei Tastaturen. Ein Programm zeigt Zufallsziffern an. Wer die Zahl als erstes richtig eingetippt hat, bekommt ein Punkt. Und das Eingabefeld wird bei beiden Spielen gelöscht. Wird ein Fehler bei der Eingabe gemacht, bekommt der Spieler einen Minuspunkt und der andere Spieler hat die Möglichkeit aufzuholen. Return muss keiner drücken. Die nächste Zahl kommt, sobald sie einmal richtig eingetippt wurde.
Ist wie Tetris, nur dass man keine Blöcke richtig positionieren muss, sondern Zahlen richtig eintippen. Strange...

16.03.2017

C++ Guns - std Input/output library

Filed under: Allgemein — Tags: — Thomas @ 19:03

Erstmal ein Link zum Thema. Die sind selten. Copy, load, redirect and tee using C++ streambufs
Und noch einen A beginner's guide to writing a custom stream buffer (std::streambuf)

Also ich muss ganz ehrlich sagen, diese C++ iostream library ist einfach nur scheiße.
Total.... unmöglich damit was gescheites zu machen. Ich meine jetzt nicht std::cout oder die ">>" "<<" Stream Syntax. Nein, man muss das Geschichtlich sehen. Die I/O Library wurde designt, also C++ designt wurde. Ist also neben std::vector und std::string eines der ältesten Sachen in der Sprache. Anders als std::vector und std::string sind diese std::*stream Klassen einfach zu abstrakt geworden. Aber man muss das verstehen. Damals waren virtuelle Klassen und Templates neu, und wann es zuviel des Guten ist, muss man ja auch erst noch herausfinden. So finden sich in der Dokumentation gleich 11 Klassen um Zeug aus/in Datein/String/Streams zu lesen/schreiben. Ich ziehe hier absichtlich den Vergleich zu Qt und Fortran. Ja, Spass muss sein.
Bei Qt gibt es QIODevice als Basisklasse für unter anderem QFile und QBuffer und dazu QTextstream. Wobei QBuffer/Textstream hier ein normalen String meint, so wie es std::basic_stringstream ist. Unter Fortran gäbe es write/read(fhdl/string)...

Jetzt gibt es zwischen std und Qt ganz klare Unterschiede im Design. Fangen wir mit Qt an. Das ist am pragmatischsten.
* Ich will von einer Datei lesen/schreiben, nehm ich QFile.
* Ich will von einem String lesen/schreiben, nehm ich QBuffer/QTextstream.
* Ich will diese tollen Stream "<<" ">>" Operatoren, nehm ich QTextstream.
Und jetzt kommt der clou:
Egal ob man mit Dateien oder Strings arbeitet, die Stream Operatoren funktionieren mit QTextstream immer. Eben WEIL QIODevice als Basisklasse designt wurde.

So weit so gut.

In der C++ I/O Library gibt es Ähnliche Ansätze.
* Ich will von einer Datei lesen/schreiben, nehm ich basic_fstream.
* Ich will von einem String lesen/schreiben, nehm ich basic_stringstream.
* Ich will diese tollen Stream "<<" ">>" Operatoren, ja, sind schon dabei.
Und jetzt kommt der clou:
Egal ob man mit Dateien oder Strings arbeitet, die Stream Operatoren funktionieren immer. Eben WEIL basic_ostream und basic_istream als Basisklassen designt wurden.

Jetzt geht die c++ I/O Library weiter und macht noch die Unterscheidung, ob man nur lesen, nur schreiben, oder beides gleichzeitig will. Und wie es die Philosophie einer streng typ basierten Sprache so will, drückt man das in dem Typ einer Variable aus. Und nicht (nur) mit einer Laufzeit Variablen.

Aus diesem Grund gibt es neben basic_fstram auch basic_ifstram und basic_ofstram.
Und neben basic_stringstream auch basic_istringstram und basic_ostringstream. Und natürlich, neben basic_iostram auch basic_istram und basic_ostram. Damit sind 9 von 11 Klassen gefunden.

Schwachsinn? IMO! Damalig geiles Design um zu zeigen was in der Sprache steckt? Absolut!
Wir kommen zu den Templates.
Ahso, beinah hätte ich Fortran vergessen.
Bei Fortran gibt es write/read(fhdl/string).

Kennt ihr den Unterschied zwischen QString und QBytearray? Beides wird benutzt um "Text" darzustellen. Und QString ist sozusagen die Standardklasse bei Qt. Intern wird ein 16bit Character Type benutzt, statt dem sonst üblichen 8 bit. Das hat einfach den Grund, weil Qt Weltweit agiert. Und entsprechend auch Weltsprachen darstellen muss. Nun gibt es in der Welt viel mehr als 256 darstellbare Zeichen. Also wird ein 16bit Character genutzt.

Der C++ Erfinder hat das 1984 schon gesehen und die I/O Library entsprechend vorbereitet. Ob das die C++ Programmierer nun auch alle verstanden haben, sei mal dahin gestellt. Der Character Typ, ob 8bit oder 16bit, kann über Templates festgelegt werden. Die Anzahl der Klassen steigt also nicht, nur ihr Typ ändert sich.
So entspricht QBytearray std::string und QString entspricht std::wstring. Für alle vorgestellten std steram Klassen gibt es eine "normale "std::istraem" und eine "std::wistrem" Klasse. std::strinstream und std::wstringstream u.s.w.

Man sieht deutlich, dass die Information bei C++ im Type steckt. Ob pragmatisch oder schlecht sei dahin gestellt.
Bei Fortran gibt es write/read(fhdl/string) und hast du es mit ausländische Sprachen zu tun, hast du Pech gehabt.

Es fehlen noch std::basic_ios und std::ios_base. Bei Qt gibt es keine direkten Vergleichs Klassen, da die Implementierungsdetails versteckt sind. Aber bei C++ 1984 war das überhaupt noch kein Thema. Also, std::basic_ios ist eine Basisklasse von allen bisher vorgestellten std:: Klassen. Jede Funktion, die std::basic_ios bereit stellt, haben auch alle anderen Funktionen. Das sind hauptsächlich Funktionen für die Fehlerbehandlung oder den Status des internen Buffers.
Wie gesagt, das ist bei Qt alles weg-abstrahiert. Interne Buffer sind da irgendwo und machen ihren Job. Das ist für den Qt Anwender auch total egal. Hauptsache, man kann lesen/schreiben.
Aber C++ hat ja als Ziel, dass man alles tun kann. Also muss man auch die ganze Kontrolle haben. Und dennoch den Kompfor, dass man sich nicht selbst in den Fuß schießt. Naja.
Wenn bei Qt ein String NICHT in ein Integer konvertiert werden kann, dann passiert entweder garnichts, oder eine optionale logische Variable wird auf false gesetzt.
Bei C++ gibt es die Funktionen good() eof() fail() bad() oder Exception, um zu überprüfen ob alles geklappt hat. Bei Qt gibt es okay, oder nicht okay; zwei Möglichkeiten. Bei C++ gibt 16. Naja, fast. Wie immer ist C++ detaillierter. Wenn etwas nicht good() ist, dann ist es entweder eof() oder fail() oder bad(). Klar, oder?
Aber wenn eof(), dann ist das nicht unbedingt fail() oder bad(). Das Ende der Datei erreicht? Absolut kein Problem. Nun, zwischen fail() und bad() liegt der Unterschied, dass bad() schlechter als fail() ist. bad() impliziert fail(). Wenn bad() ist, dann geht es nicht weiter. Dann geht die Welt unter. Dann ist alles vorbei. Aber mit fail() koennen wir weiter leben. Eine Konvertierung von String nach Integer failt zwar, aber sonst ist alles okay und wir können weiter machen.

Wann genau welcher Fehler auftrifft, kann in der IObase Doku nachgelesen werden.

Nun gibt es seit C++ auch Exception. Alle Google Entwickler mögen hier jetzt bitte sterben. Exceptions bieten die Möglichkeit den Programmteil wo "alles ist gut", von dem Programmteil "Fehlerbehandlung" zu trennen. Liest man dann den Code von Oben nach Unten, liest man nur den Teil, der ausgeführt wird, wenn kein Fehler passiert. Nur das interessiert einem meistens.
Die Exceptions ignoriert man bei Qt komplett. Nun, das ist ein Weltweites Framework welches versucht es jedem recht zu machen. Es sei ihnen verziehen.
Bei C++ hat man, wie immer, die Wahl.

Kommen wir nun langsam zum Kern der Sache, warum ich mich damit beschäftige. Ich möchte einen XYZ Reader bauen, der stumpft pro Zeile drei Zahlen liest. Es sollen immer drei pro Zeile sein. Und ich möchte das mit pure C++ machen, weil Qt nicht überall installiert ist.
Das ist auch absolut kein Problem.


stream >> var1 >> var2 >> var3;

Das funktioniert bei C++ genauso wie bei Qt genauso wie bei Fortran. Nur da müsste man read() schreiben.
Aber was ist mit der Fehlerbehandlung? Wenn irgendwas schief läuft, dann will ich genau wissen, WAS und WO und WARUM es schief lief. Auf dumm herum Raten hab ich kein Bock. Lebenszeit und so.
Ja Scheiße, das funktioniert weder bei C++ noch bei Qt noch bei Fortran gut. Bei Fortran und Qt gib es zwar Statusvariabeln ob etwas schief lief, aber DAS etwas schief lief, muss man noch selbst prüfen. Bei C++ ist das DAS mit exceptions zwar automatisiert, aber es bleibt immer noch das WAS.
Und das WO, z.b. mit Zeilennummern, muss man alles noch selbst implementieren. Ich hätte gerne eine Fehlermeldung wie: "Datei xyz. Zeile: 123. String 'scheisse' kann nicht nach interger konvertiert werden". Ja dann ist alles klar. Und nicht "iostream error". Den Error kanntes dir sonst wo hin stecken.

Wir können dieses Verhalten natürlich selbst implementieren. Eine Zeile einlesen und parsen. Bei Fortran hat man gleich das Problem: Wie groß muss der Buffer sein? BUMM FAIL.
Bei Qt wird eine Kopie für den Buffer angelegt. BUMM Performance FAIL.
Bei C++ kapiert kein Aas wie er das implementieren soll. BUMM BRAINFAIL.

C++ soll performant sein. Und damit meinte ich nicht. "Lade die Webseite mit 4 Bildern und 3 Textzeilen in unter 10 Sekunde". Das ist für Gehirn entfernte Idioten. Ich meinte so etwas wie "Verarbeite eine Milliarden Buchstaben in einer Sekunde".
Und dazu gehört auch, dass Eingabe Daten nicht erst unnütz in einem extra Buffer geladen werden, sondern, dass gleich auf den Daten gearbeitet wird.

Und genau hier komme ich zur abstrakten Vorgehensweise von "Lese drei Zahlen pro Zeile ein". Ein Zeilenende ist durch ein \n gekennzeichnet. Es muss also erst ein Abschnitt gefunden werden, von wo bis zum nächsten \n gültige Daten vorliegen (unformatiertes lesen). Und dann erst kann auf diesem Abschnitt versucht werden, formatiert zu lesen. Also 3 Zahlen zu extrahieren. Diese Erkenntnis selbst, ist für viele schon Brainfuck genug.

Es sollen also folgende Fehler erkannt werden.
EOF bevor 3 Zahlen fertig gelesen wurden.
Newline char bevor 3 Zahlen fertig gelesen werden.
Konvertierungs Fehler von String nach Zahl.

So, und wie wird das Umgesetzt?
Erstmal die Version, welche eine Zeile in ein String einliest und dann parst.
Datei werden die Daten nicht gespeichert, nur eingelesen, geparst und verworfen.
Testdatei mit 772MB, 28895639 Zeilen a 3 Zahlen.

Workrechner:
real 0m44.049s
user 0m43.892s
sys 0m0.176s
17.55 MB/s

real 0m44.965s
user 0m44.760s
sys 0m0.216s
17.16 MB/s

Heimrechner:
real 2m0.688s
user 1m58.776s
sys 0m1.228s
6.38 MB/s

real 1m54.953s
user 1m54.172s
sys 0m0.628s
6.66 MB/s

Wir haben also ein Durchsatz von 17 MB/s bzw. 6 MB/s


std::ifstream ss(fileName);
if(!ss.is_open()) throw();
try {
  ss.exceptions(std::ios_base::failbit);
  for(size_t i=0; i < n and ss.good(); ++i) {
    std::getline(ss, line);
    std::istringstream ss2(line);
    ss2.exceptions(std::ios_base::failbit);
    ss2 >> data[0];
  }
} catch(std::ios_base::failure& ex) {
...
}

Zwei Input streams zu erzeugen kommt mir wrong vor. Aber der eine liest unformatted und der andere formatted.
Aber der formated stream kann ja auf dem Buffer vom unformatted stream Arbeiten. Oder so ähnlich. Eigentlich, ist der newline character ein Zeichen fuer den formated stream. Und nun kann man std::ctype und die std::locale von std::istream so änderen, dass '\n' nicht als whitespace anerkannt wird. Damit es es möglich, Zahl für Zahl einzulesen aber bei '\n' ist schluss. Dummerweise muss man im dann von Hand noch '\n' und restliche whitespaces einlesen. Das führt wieder zu versteckte Fehler. Und die virtuellen Fehler sind eh Performance Killer. Das MUSS besser gehen.

Workrechner:
real 0m32.871s
user 0m32.600s
sys 0m0.276s
23.49 MB/s

real 0m32.776s
user 0m32.576s
sys 0m0.200s
23.55 MB/s

Heimrechner
real 1m37.050s
user 1m33.396s
sys 0m1.512s
7.95 MB/s

real 1m39.499s
user 1m31.360s
sys 0m1.524s
7.76 MB/s

Immerhin von 17 auf 23 bzw. von 6 auf 7 MB/s hoch.

So, habe mit mmap die Inhalt direkt ueber den Speicher zugreifbar gemacht. Zum convertieren nehme ich std::strtod und bals std::from_chars. Die arbeiten direkt auf dem Speicher. Kein dummen rumkopieren in irgendwelche Buffer mehr. Dann schaun wir mal:

Heimrechner
real 0m33.893s
user 0m31.112s
sys 0m0.876s
22.77 MB/s

real 0m30.617s
user 0m29.564s
sys 0m0.356s
25.21 MB/s

Nu schau sich das mal einer an. Von 7 MB/s auf 25 MB/s hoch. Faktor 4 schneller. Dann müsste mein Workrechner locker mit 60 MB/s arbeiten. Mit eingeschalteter Compiler Optimierung ist sogar noch etwas mehr drin.

Der Cluster parst bei eingeschalteter Optimierung die 772 MB in 9.6sec. Das macht stolze 80.4 MB/s!

14.03.2017

C++ Guns - Funktion return unterschiedliche lambda Funktionen

Filed under: Allgemein — Tags: — Thomas @ 06:03

Na sicher geht das. Zur Compilezeit wie auch zur Laufzeit.
Zur Compilezeit wäre if constexpr angebracht, aber das kann mein Compiler noch nicht. Mit Templates wär das wieder overload. Also nehm ich stink normales Funktion überladen mit Tags.


struct Add_tag {};
struct Mult_tag {};

auto func1(int x, Add_tag) {
    return [x](int y){ return x+y; };
}

auto func1(int x, Mult_tag) {
    return [x](int y){ return x*y; };
}

int main() {
    cout << func1(2, Mult_tag())(3) << "\n";
    return 0;
}

Zur Laufzeit fällt mir std::function ein. Mit den üblichen Performancefressern.


enum class Operator {ADD, MULT};

std::function func1(int x, Operator op) {
    switch(op) {
    case Operator::ADD:  return [x](int y){ return x+y; };
    case Operator::MULT: return [x](int y){ return x*y; };
    }
}

int main() {
    Operator op = Operator::MULT;
    cout << func1(2, op)(3) << "\n";
    return 0;
}

Hübsch? Naaah. Braucht man? Bisher nicht.

11.03.2017

C++17 Guns - std::optional Part IV

Filed under: Allgemein — Tags: — Thomas @ 13:03

Der Übergang von cpl::optional nach std::optional ist schnell gemacht. Nur beim Zugriff ändert sich sichtlich etwas. Wir können operator*, operator->, value() oder value_or() nutzen. Das sieht alles nicht gut aus:


if(DMP and SMP) {
    if(fuzzyEqual(abs(DMP->point.z - SMP.value().point.z), schacht.tiefe) == false) {
        cout << "Redundante Information Schachttiefe vs DMP-SMP";
    }
}

Bei operator-> denkt man gleich an Pointer. Bisher war es aber immer operator. gewesen. Und der extra Umweg über value() ist auch too verbose. Wenn wir mehr Richtung funktionaler Programmierung denken, dann ergibt sich die Möglichkeit, den ekligen Zugriff in einer Funktion zu verstecken. Diese Funktion bekommt beide Punkte, und eine Funktion, was mit den Punkten gemacht werden soll.
Das könnte in etwa so aussehen:


template
void maybeBoth(const optional& x, const optional& y, Function func) {
    if(x and y) {
        func(*x, *y);
    }
}

maybeBoth(schacht.DMP(), schacht.SMP(),
    [&schacht](const Punkt& DMP, const Punkt& SMP) {
        if(fuzzyEqual(abs(DMP.point.z - SMP.point.z), schacht.tiefe) == false) {
             cout << "Redundante Information Schachttiefe vs DMP-SMP";
        }
    }
);

Hmm mit der Einrückung hab ich so meine Probleme. Jedenfalls wäre das die kürzeste Variante, denn der maybeBoth Ausdruck ist nur eine Zeile. Der komische Zugriff ist weg, dafür müssen wir eine Funktion, oder ein Lambda definieren. Welches wiederum ein Funktionskopf mit Parameter braucht. Das ist wieder sehr verbose... In anderen Sprachen ist die Syntax angeblich viel besser.
Hm es ist anders zu lesen. Ungewohnt. Funktional eben. Da steht kein if() mehr. Das "Wenn" steckt jetz in "maybeBoth". Aber maybe was? Dass schacht.DMP() ein optional zurück gibt, ist hier nicht ersichtlich. Die logische Verknüpfung, dass DMP UND SMP existieren müssen ist hier nicht ersichtlich. Sicher, man kann das in der Dokumentation so festhalten von maybeBoth() und nach einer weile hat man das drauf.
Aber für ein winziges Stück Code, was ich maximal schnell hinschreiben will (Zeit ist Lebenszeit), ist das doch nichts. Womöglich sollte ich "maybe" durch "if" ersetzen. Also ifBoth(DMP, SMP)? Oder ifBothExist(DMP, SMP) ? Sieht schon besser aus. Voll viel Arbeit...

Aber nochmal kurz zurück an den Anfang. Was gewinne ich dadurch? Dass T in cpl::optional default construktible sein muss? Hm. MUSS es das wirklich? Durch die Vererbung wäre das doch angebracht. Sicher funktioniert cpl::optional nicht bei final Klassen. Es ist eben keine generelle Lösung, sondern eine, die jeder schnell versteht und umsetzen kann.

Eine Sache bleibt noch aus. Was passiert, wenn ich neben DMP und SMP auch noch RAP und SA brauche? Und ich möchte nicht die Schleife in Schacht::x vier mal laufen lassen. In nur einer Schleife soll überprüft werden, ob alle geforderten Punkte existieren und auf einmal, alle als optional, zurück gegeben werden. Ob das gut funktioniert?

C++17 Guns - std::optional Part III

Filed under: Allgemein — Tags: — Thomas @ 12:03

So, kommen wir zum dritten Teil. Zu Versuchs und Lernzwecken habe ich meine eigene cpl::optional Klasse geschrieben, die genau das macht, was ich zur Zeit brauche. Ich mache mir absichtlich das Leben einfach und vordere, dass der Typ default constructible sein muss. Hält cpl::optional kein Wert, so ist ein default Wert gespeichert. Das hat die schon beschriebenen Nachteile, aber man muss ja pragmatisch denken. Die Syntax soll nicht so einfach wie möglich sein, sondern so einfach, wie sie vorher schon war. Zum Vergleich zeige ich den alten Code, eine funktionale Version und eine neue Version

Alter Code:


    bool hasDMP = false;
    Punkt DMP;
    bool hasSMP = false;
    Punkt SMP;
    for(const Punkt& punkt : schacht.punkte) {
        if(punkt.attr == PunktAttributAbwasser::DMP) {
            hasDMP = true;
            DMP = punkt;
        } else if(punkt.attr == PunktAttributAbwasser::SMP) {
            hasSMP = true;
            SMP = punkt;
        }
    }

    if(hasDMP and hasSMP) {
        if(fuzzyCompare(abs(DMP.point.z-SMP.point.z), schacht.tiefe) == false) {
            cout << "Redundante Information Schachttiefe vs DMP-SMP";
        }
    }

Diese Version hat folgende Nachteile und sleeping errors (Phew wo fang ich denn an).
a) WAS vs. WIE. In dem Code steht, WIE die zwei Punkte gefunden werden und danach WIE damit weiter verfahren wird. Aber es steht nicht da WAS überhaupt gemacht werden soll.
b) SMP und DMP sind ähnlich geschriebene Variablen die leicht verwechselt werden können. Das sind aber vorgegebene Abkürzungen die es einzuhalten gilt. Viel schlimmer aber sind die beiden zusätzlichen bool Variablen. Es können die bool und Punkt Variablen in jeder Kombination vertauscht werden.
c) Die Lebenszeit der DMP und SMP Variablen ist zu lange. Nach der letzten if() geht der Code normalerweise noch weiter. Hier könnte man eine der Variablen wieder benutzen, obwohl das gar nicht beabsichtigt ist. Ein ähnliches Problem hat die Sprache C mit der Laufvariable i in for() Schleifen. Das ist seit 1984 in C++ nie wieder ein Fehler gewesen.
d) RAII nicht beachtet. Erst wird der Punkt default initialisiert, und später vllt. einem anderen Punkt aus dem Schacht zugewiesen. Statt gleich die Zuweisung zu machen.
e) Und letztendlich wird eine Kopie des Punktes erzeugt. Das ist in diesem Fall aber das kleinere Übel. Referenzen, Indices, Iteratoren oder Pointer können alle invalid werden, sollte sich die Punkte in dem Schacht zwischenzeitlich ändern.

Hier sind Lösungsvorschläge:
a) Die Schleife muss in eine Funktion ausgelagert werden.
b) Punkt und bool Variable in ein neuen Datentyp koppeln.
c) Mehrere Möglichkeiten:
Scope durch zwei Klammern {} verkleinern.
Code in eine weitere Funktion packen.
C++17 "Selection statements with initializer" nutzen.
d) Auch (teil)gelöst durch Schleife in Subroutine.

Punkt a) ist trivial. Das sollte etwa so aussehen: auto DMP = punkt.DMP();
Punkt b) ist durch ein optional lösbar. Ich zeige Code mit std::optional und cpl::optional.
Punkt c) C++17 ist natürlich vorzuziehen.
Punkt d) Da cpl::optional default constructible brauchst ist hier RAII nicht so zu 100% enigehalten.

Fangen wir an:


cpl::optional<Punkt> Schacht::DMP() const {
    for(const Punkt& punkt : punkte) {
        if(punkt.attr == PunktAttributAbwasser::DMP) {
            return punkt;
        }
    }

    return {};
}

Im Erfolgsfall wird der Punkt zurück gegeben, sonst ein default cpl::optional, welches keine Daten hält. Für SMP() analog. Um zu testen ob ein std::optional ein Wert hält, gibt es die Funktionen has_value() oder operator bool. Ich möchte gern die Funktion beim Namen nennen, um den Code lesbarer zu halten. Aber punkt.has_value() ist.. seltsam. Jeder Punkt hat doch Werte? Daher nehme ich für cpl::optional lieber den Funktionsnamen exist(). Und so schauts aus:


auto DMP = schacht.DMP();
auto SMP = schacht.SMP();

if(DMP.exist() and SMP.exist()) {
    if(fuzzyCompare(std::abs(DMP.point.z-SMP.point.z), schacht.tiefe) == false) {
        cout << "Redundante Information Schachttiefe vs DMP-SMP";
    }
}

Das ist schon wesentlich lesbarer. Hier empfiehlt sich die NUtzung von "auto", sonst müsste man umständlich cpl::optional schreiben. Also, Punkt a) b) d) erfüllt. Für c) nehme ich C++17, das sieht dann so aus:


if(auto DMP = schacht.DMP();  DMP.exist()) {
    if(auto SMP = schacht.SMP(); SMP.exist()) {
        if(fuzzyEqual(std::abs(DMP.point.z-SMP.point.z), schacht.tiefe) == false) {
            std::cout << "Redundante Information Schachttiefe vs DMP-SMP";
        }
    }
}

Jetzt bin ich mir nur nicht sicher, ob die Initialisierung in der if() auch für mehrere Variablen funktioniert, oder ob der Compiler noch buggy ist. Aber jedenfalls, aus den anfänglichen 19 Zeilen Code sind es 7-8 geworden. Ein WIE wurde in ein WAS verwandelt. Der zweite Teil mit dem fuzzyEqual lass ich mal so stehen.

Hier die Implementierung von cpl::optional


namespace cpl {
template<class T>
struct optional : public T {
    static_assert(is_default_constructible<T>::value);
    optional() = default;

    optional(const T& t)
        : T(t), vorhanden(true)
    {

    }

    explicit operator bool() const {
        return vorhanden;
    }

    bool exist() const {
        return vorhanden;
    }

    bool vorhanden = false;
};
}

Das nächstemal geht es mit std::optional weiter

10.03.2017

C++17 Guns - std::optional Part II

Filed under: Allgemein — Tags: — Thomas @ 11:03

Wie schon im ersten Teil berichtet, suche ich vernünftige Anwendungsfälle für std::optional.
Dabei ist mir aufgefallen, dass schon viele Variablen implizit optional sind/behandelt werden. Ohne, dass eine extra Variable wie z.B. bool vorhanden; vorhanden ist. Nehmen wir z.B. ein Adressbuch. Wenn der Adresszusatz nicht angegeben wurde, ist der String einfach leer. Ein std::optional<std::string>> adresszusatz; wäre redundante Information. Einmal explizit im std::optional und einmal implizit im string::size.

Bei Strings ist das einfach. Doch was ist mit Zahlen? Wenn die Hausnummer in der Adresse als Integer gespeichert wäre, und es fehlt die Information, welche Zahl ist in der Variablen hausnummer dann gespeichert? int hausnummer = 0; Null als Default? Oder lieber -1? Oder Undefiniert? Oder MAX/MIN Integer Wert? Bei floats gäbe es NaN...

Die Idee ist, dass Zahlen einen validen Wertebereich haben. Negative Hausnummern gibt es einfach nicht. Auch ist es sehr unwahrscheinlich, dass eine Straße zwei Milliarden Häuser hat. Die Information, ob der Wert vorhanden ist, oder nicht, ist im Wert selbst kodiert. Das widerspricht aber dem C++ Ziel, eine generalisierte Sprache zu sein. Die allgemeingültige Lösung für std::optional kann daher nicht so implementiert sein, dass die 'vorhanden' oder 'nicht vorhanden' Information in der eigentlichen Variablen mit gespeichert wird.

Wir würde eine Implementation von "optional" aussehen?
Eine zusätzliche boolean Variable bool vorhanden = false; wäre denkbar. Aber das kleine bool verhunzt das Speicherlayout der Klasse. Man könnte nur einen Pointer nehmen. Ein nullptr bedeutet, dass der Wert nicht vorhanden ist. Aber dann haben wir das owning Problem wie mit dem std::shared_ptr auch. Was ist, wenn std::optional eine Kopie der Variable hält? Problem 1 gelöst. Problem 2: Welcher Wert hat denn das optional, wenn es kein Wert gibt? Ein default Wert. Welcher? Nicht jede Klasse hat einen default ctor oder kann überhaupt einen haben.

Ich sehe, default Werte, default ctors und std::optional und string::size (vector::size) haben viel gemeinsam. Da kommt auch gleich die RAII (Ressourcenbelegung ist Initialisierung) Idee daher. Man schreibt nicht einfach std::string s; int i; oder int i=0; Sondern belegt die Variable gleich mit einem gültigen Wert.
std::string s = func(); int i = func(); Was aber, wenn func() keinen Wert zurück liefern kann, weil es keinen gibt? Das muss nicht gleich ein Fehler sein. Datensätze sind nunmal nie vollständig. Das ist das selbe Problem wie oben besprochen.

Ich persönlich fühle mich nicht wohl mit dem Gedanken, überall im Code optionals zu haben, die existieren könnten, oder nicht. Die Variationen gehen exponentiell mit der Anzahl der Variablen. Das will niemand und ich bezweifle, dass man das überhaupt braucht.

Also mir kommen da zwei Möglichkeiten in den Sinn. Der Erste, wie oben schon angedeutet, einen gültigen Wertebereich definieren. Viele Variablen bilden ja irgendwas aus der Realität ab. Z.B. eine Höhenangabe auf der Welt. Was ist der höchste Punkt? Irgendwas mit 8000m. Was ist der tiefste Punkt? Irgendwas mit -11000m. Man kann also eine noDepthValue definieren mit -99999m und ihn als default Wert für Höhenangaben nutzen. Das lässt sich mit dem Typesystem auch leicht implementieren. using Height_t = double; Oder beliebig kompliziert. Das Funktioniert mit Koordinaten genauso. Im sphärischen System gibt es keinen Rechtswert größer als 180grad. Vorraussetzung dafür ist, dass das "Ding" in der Realität auch genau diesen Typ in C++ bekommt. Also kein double x; mehr. Lieber Coordinate x; So können auch gleich SI Einheiten mit einbezogen werden. Aber das ist ein anderes Thema. Das ist auch gleich Performant und SSE freut sich, wenn die Daten alignt rein kommen.

Ist die erste Möglichkeit schon sehr Domänenspeziell, brauchen wir auch eine allgemeingültige C++ Philosophie Lösung. Hier orientiere ich mich an std::string::size bzw std::vector::size. Wie schon oben Erwähnt, ist die Information, ob ein String leer ist oder nicht, in der size Variable in der std::string Klasse abgelegt. (Und nicht, wie bei C, im den eigentlichen String Daten selbst). Was hindert mich daran, das für alle anderen Daten auch zu tun? Für das Beispiel nehme ich nochmal die Adresse.


struct Name {
  string vorname, nachname;
};
struct Adresse {
  vector<Name> name;
  vector<string> strasse;
  vector<int> hausnummer;
  vector<string> stadt;
};

Na, ist das nichts? ;) Ich finde das sehr realistisch. Hat die Adresse keinen hausnummer, so ist der vector einfach leer. Ein leerer Vector führt bei keiner Library zu Problemen. Jede for() Schleife ist glücklich damit. Da steckt das if(vorhanden) schon drin. Und wer sagt denn bitte schön, dass eine Person nur einen Namen haben darf? Wenn man heiratet, ändert sich der Nachname. Wenn man die Identität wechselt, ändert sich der Name. Die Freunde nennen einem meist nicht beim vollen Namen. Im Internet hat man Nicks. Man kann sich auch als jemand anderes ausgeben. In anderen Ländern wird sein Name womöglich leicht anders geschrieben und ausgesprochen. Soll ich weiter machen? Das selbe gilt natürlich auch für die restlichen Variablen. Hier wäre also sogar ein Zeitfaktor mit implementiert ;)
Absolut übertrieben und Anwendungsfern? Ja aber, genau das wollen wir doch mit c++.

09.03.2017

C++(+)

Filed under: Allgemein — Tags: — Thomas @ 18:03

"c++17 is out now."
"c++20 we know if you think on macros. people will die if you dont stop it"
"c++30 alle pepole who has written code in FORTRAN77 are dead now"
"c++35 there are only 10 kinds of languages out there. strong types and not"
"c++36 only strong types left"
"c++42, this is the answer. what was the question?"
"c++64 good damn this is an old language. huh? you are right $/§. did you remeber c++32? oh yeah that was fun. swap it. what would you get? 23!!aaaargghl"

C++17 Guns - std::optional

Filed under: Allgemein — Tags: — Thomas @ 13:03

Ich glaube, ich sehe einen Verwendungszweck für std::optional.
Ein hydraulischer Schacht kann mehrere Punkte besitzen. Z.B. für Schachtdeckel (DMP) und Schachtmittelpunkt (SMP). Nun sind Datensätze nie vollständig, und ich muss mit beiden Punkthöhen weiter rechnen, wenn sie denn existieren. Das lässt sich natürlich leicht über ein boolean hasDMP realisieren. Und einer Schleife die über alle Punkte läuft. Natürlich will niemand im Code 100x die selbe Schleife schreiben. Es würde auch dem "WAS und nicht WIE" Prinzip widersprechen. Also wird die Schleife in eine Funktion ausgelagert. Die Schleife gibt den Punkt zurück und ein boolen, ob der Punkt auch existiert.


auto [DMP, hasDMP] = schacht.DMPpunkt();
auto [SMP, hasSMP] = schacht.SMPpunkt();
if(hasDMP and hasSMP) {
    distanceTo(DMP, SMP);
}

Ist ganz nett, und durch das "auto" auch relativ kurzer Code. Aber es geht mit std::optional wohl noch kürzer. Möglicherweise so:


auto DMP = schacht.DMPpunkt();
auto SMP = schacht.SMPpunkt();
if(DMP and SMP) {
    distanceTo(DMP, SMP);
}

Ist hier die Semantik zu abstrakt? Ist intuitiv klar was if(DMP) bedeutet? Der Typ der DMP und SMP Variabel ist schon variabel. Das ist schon brainfuck für viele C++ Programmierer aber lustigerweise Schokolade für die Python Programmierer. Aber jetzt ist sogar die Existenz der Variabel variabel.

Und wie sieht es mit der Performance aus? Kann std::optional so implementiert werden, dass kein new() aufgerufen wird?

Nachteil: Höhere Laufzeit, da zwei mal die Schleife gestartet wird. Vorschlag: tupel Schacht::punkte(Args...) Hm?

C++17 Guns - std::make_tuple - warum so umständlich?

Filed under: Allgemein — Tags: — Thomas @ 11:03

Seit C++11 gibt es den Typ std::tuple und std::pair. Das erstellen derselben war über Hilfsfunktionen std::make_tuple() bzw. std::make_pair() möglich:


auto x = std::make_tuple(1, 'X', 3.14);

Die Vereinfachung wurde jetzt im C++17 Standard aufgenommen. Nennt sich P0091R3 - Template argument deduction for class templates.

This paper proposes extending template argument deduction for functions to constructors of template classes

GCC unterstützt es seit Version 7. Damit ist der obige Code nun intuitiver wie folgt zu schreiben:


auto x = std::tuple(1, 'X', 3.14);

Die Sprache wird immer einfacher...

« Newer PostsOlder Posts »

Powered by WordPress