Vortrag über Ideen für eine neue Sprache für Spieleentwickler [youtube]



  • hustbaer schrieb:

    Either=boost::variant oder ein beliebiger anderer Discriminated Variant Container.
    Somit hätten wir den Teil "wie sowas in Haskell gemacht wird" schonmal abgehakt.

    Bei Either in Haskell kann man aber über die Monad-Instanz und mittels do-Notation Code schreiben, bei dem der Fehler (also ein Left -Wert) automatisch propagiert wird, ähnlich wie bei Exceptions. Allerdings lokal in einem monadischen Block und nicht unerwartet über komplett unzusammenhängende Programmteile hinweg. So etwas geht mit boost::variant o.ä. nicht, und selbst wenn man die monadischen Operationen anbieten würde, wäre die Syntax so umständlich, dass es niemand nehmen würde. Damit vereinigt Either die Vorteile von Fehlerbehandlung über Rückgabewerte (einfache Struktur, typsicher, weniger Bugs) und Exception (keine manuellen Checks nach jedem Funktionsaufruf nötig).

    Übrigens ist das auch das, was auf der HaskellWiki-Seite beschrieben wird. Der dortige Exceptional -Typ ist isomorph zu Either und ExceptionalT ist analog zu ExceptT , dem Monad-Transformer von Either .

    Exceptions im klassischen Sinne gibt es in Haskell übrigens auch, allerdings nur in IO -Code und manche halten ihre Existenz auch für einen Fehler. Wenn möglich, sollte Either / ExceptT bevorzugt werden.



  • hustbaer schrieb:

    Maybe ist kein Ersatz für Exceptions, weil man damit nicht kommunizieren kann was eigentlich schief gegangen ist.
    Wählst du absichtlich Beispiele die vermutlich 90% der Forenuser erst googeln müssen?
    Maybe=boost::optional
    Either=boost::variant oder ein beliebiger anderer Discriminated Variant Container.
    Somit hätten wir den Teil "wie sowas in Haskell gemacht wird" schonmal abgehakt.
    Dass speziell der Umgang mit Discriminated Variants in C++ vermutlich viel aufwendiger zu schreiben (und auch zu lesen) ist, ist wieder eine andere Sache. Geben tut es diese Mittel auf jeden Fall.
    Und ich weiss auch nicht was das mit dem Thema Exceptions zu tun haben soll.
    Exceptions gibt es in Haskell genau so.
    Hier
    http://www.haskell.org/haskellwiki/Exception
    steht sogar schön beschrieben wann man Exception verwenden sollte.

    An exception denotes an unpredictable situation at runtime, like "out of disk storage", "read protected file", "user removed disk while reading", "syntax error in user input". These are situation which occur relatively seldom and thus their immediate handling would clutter the code which should describe the regular processing. Since exceptions must be expected at runtime there are also mechanisms for (selectively) handling them.

    Lässt sich mMn. 1:1 auf C++ übertragen.

    Maybe/Either sind nicht "nur" optional/variant. Sieh dir an, wie man damit arbeitet. Und zwar nicht nur am Ende (da wo try/catch stehen wuerde), sondern wie Errors weitergegeben werden. Das sind im Prinzip Error-Codes, nur besser.

    Aus dem Code der von dir verlinkten Seite:

    main :: IO ()
    main =
       do result <- runExceptionalT (readText "test")
          case result of
             Exception e -> putStrLn ("When reading file 'test' we encountered exception " ++ show e)
             Success x -> putStrLn ("Content of the file 'test'\n" ++ x)
    

    Das ist auch nur ein besser benanntes Either, das eben entweder Exception e oder Success x sein kann. Steht auch hier:

    The great thing about Haskell is that it is not necessary to hard-wire the exception handling into the language. Everything is already there to implement the definition and handling of exceptions nicely.

    hustbaer schrieb:

    Kellerautomat schrieb:

    Zu deinem std::string Beispiel sag ich einfach mal nix, da du offensichtlich meine Posts garnicht richtig gelesen hast. Du findest die Antwort dort.

    Nein, finde ich nicht. Zeig sie mir.

    Eine fehlgeschlagene Memory-Allokation ist ein Fatal-Error. Also Exception.

    hustbaer schrieb:

    Kellerautomat schrieb:

    Exceptions verwenden kann schonmal Sinn machen - WENN man einen guten Grund dafuer hat. Aber Exceptions einfach so als universelle Loesung abzutun ist einfach nur Unsinn.

    Ach, jetzt auf einmal?
    BTW: der gute Grund dafür Exceptions zu verwenden steht in dem von mir gerade von haskellwiki zitierten Abschnitt beschrieben.

    Vielleicht hab ich zu kurz gedacht. Soll mal vorkommen.

    hustbaer schrieb:

    Wobei es bei "File konnte nicht geoeffnet werden" genau genommen zwei Fälle zu unterscheiden gibt:

    1. Es ist zu erwarten dass das File existiert, und wenn es nicht existiert (oder aus anderen Gründen nicht geöffnet werden kann), dann ist es ein "unerwartetes Ereignis" das als solches behandelt werden sollte.
      Den Fall hast du z.B. wenn du Asset Files in einem Spiel lädst, ein File aufmachen willst das der User in einem Open-File Dialog ausgewählt, ein Logfile mit "open or create" aufmachen willst etc.
    2. Man weiss nicht so genau ob das File existiert bzw. ob man es aufmachen kann.
      Den Fall hast du z.B. wenn du ein Config-File aufmachen willst welches aber optional ist. Gibt sicherlich noch etliche andere "Standardfälle", aber mir fällt jetzt auf die Schnelle kein weiterer ein.

    Und auf (1) trifft nun genau das zu was haskellwiki über Exceptions schreibt: die Fälle kommen nicht oft vor, und man will sich seinen Code nicht damit zumüllen überall entsprechende Checks reinzuschreiben.
    Und, nur weil es gerade zufällig passt: die (1) Fälle kann man auch durchaus als "Ausnahmen" bezeichnen.

    Was (2) angeht: dafür macht man in C++ idealerweise das selbe wie in Haskell, nämlich TryXxx Funktionen. Also z.B. optional<File> TryOpenFile(string path, IOError& out_error) o.Ä. Idealerweise mit einem Overload ohne den Output-Parameter, denn der aufgetretene Fehler interessiert oft gar nicht.
    Du scheinst (1) aber komplett zu ignorieren, und beschränkst dich auf die wesentlich selteneren Fälle (2).

    Hast du wohl Recht. Context matters.

    hustbaer schrieb:

    Kellerautomat schrieb:

    hustbaer schrieb:

    Kellerautomat schrieb:

    shared_ptr: Hab ich persoenlich noch nie gebraucht, kann also nicht nachvollziehen was das bringen soll. Und natuerlich kann man mit RAII use-after-free Fehler bauen. Das verhindert man am besten, indem man ein API ordentlich designed.

    Uiiiiiii. Wieder kein Argument. Diesmal nicht weil der Zusammenhang fehlt, sondern weil es einfach nur eine Behauptung ist. Der ich widerspreche: geht halt nicht immer. Und je nachdem was man so programmiert kann "nicht immer" sehr oft sein.

    Zeig mir die Faelle, wo du glaubst shared_ptr zu brauchen. Ich zeig dir, warum du ihn nicht brauchst.

    OMG, du bist echt stur. Oder unerfahren. Oder beides.

    Ein Programm kommuniziert über diverse Transport Channels (RS-232, TCP/IP Verbindung, HTTP Verbindung, ...) mit diversen Endpoints (Web-Services, Periphäriegeräte die an einem gemeinsamen Bus hängen oder verschiedenen Funktionsblöcken in diesen Geräten etc.).
    Wichtig ist nur: es gibt mehrere Endpoints die über den selben Channel angesprochen werden müssen.
    Gibt genügend reale Beispiele dafür, musst du mir im Zweifelsfall einfach glauben.
    Und da teilen sich alle Objekte die eine Verbindung zu einem Endpoint darstellen halt einfach die Ownership des Channel Objekts.

    Thread-safe Signals.
    Wenn dir das Problem nicht bekannt ist, dann lies dir die Doku zu Boost.Signals2 durch, da wird es ausführlich erklärt:
    http://www.boost.org/doc/libs/1_56_0/doc/html/signals2/thread-safety.html#idp431602368
    Der Knackpunkt ist: Das Signal muss sicherstellen dass der Slot nicht gelöscht wird während er gerade aufgerufen wird. Das Signal muss also während des Aufrufs "owner" des Slots sein. Gleichzeitig muss aber natürlich der der den Slot connected hat auch "Owner" des Slots sein, da der Slot ja sonst sofort wieder zerstört ("dicsonnected") würde.

    Billig kopierbare immutable Objekte mit Hilfe von shared_ptr<T const> bauen.
    Ist ne "Optimierung", aber eine sehr gute. Ist einfach, effizient und sicher.

    ...

    Ich hatte gehofft, du kommst mir jetzt nicht mit solchen kleinen Details (#2/#3) sondern mit richtigen Szenarien. Bei #1 wuerde ich einfach sicherstellen, dass der Channel laenger lebt.

    hustbaer schrieb:

    Kellerautomat schrieb:

    Und bevor ichs vergesse, was mich an Exceptions besonders stoert, ist, dass Interfaces einem nicht verraten, ob und welche Exceptions sie werfen koennen. Wenn ich mal eine Exception vergesse zu behandeln, die dann bei mir nie auftritt, hab ich ein Problem. Vielleicht sind Javas checked Exceptions doch nicht so schlecht.

    Na geht ja. DAS ist nämlich wirklich ein Argument. Wenn auch mMn. kein besonders gutes. Die Aufweichung von "kann Exceptions der folgenden Typen werfen" zu einfach nur "kann Exceptions werfen" bzw. "kann keine Exceptions werfen" ist nämlich in der Praxis sehr sehr ... praktisch.
    Weil es praktisch kaum managebar ist sämtliche Exception-Typen aufzulisten die etwas werfen kann. Ob mit oder ohne Sprachsupport spielt dabei kaum eine Rolle.

    Was das "Exception vergessen zu behandeln" angeht: hier nähern wir uns schön langsam dem eigentlichen Problem: nämlich dass die Klassenhierarchien der Exception von Java, C++ und C# total beknackt sind, und C++ es obendrein noch erlaubt beliebige Klassen zu werfen.
    Das sehe ich auch als Problem. Es führt aber nicht die ganze Exception-Sache ad Absurdum, sondern macht lediglich den Umgang mit Exceptions in C++ etwas lästig.

    Vielleicht checked Exceptions an API Boundaries. Also "mir egal was du intern machst, solange du mir sagst, was ich behandeln muss".



  • Kellerautomat schrieb:

    hustbaer schrieb:

    Kellerautomat schrieb:

    hustbaer schrieb:

    Kellerautomat schrieb:

    shared_ptr: Hab ich persoenlich noch nie gebraucht, kann also nicht nachvollziehen was das bringen soll. Und natuerlich kann man mit RAII use-after-free Fehler bauen. Das verhindert man am besten, indem man ein API ordentlich designed.

    Uiiiiiii. Wieder kein Argument. Diesmal nicht weil der Zusammenhang fehlt, sondern weil es einfach nur eine Behauptung ist. Der ich widerspreche: geht halt nicht immer. Und je nachdem was man so programmiert kann "nicht immer" sehr oft sein.

    Zeig mir die Faelle, wo du glaubst shared_ptr zu brauchen. Ich zeig dir, warum du ihn nicht brauchst.

    OMG, du bist echt stur. Oder unerfahren. Oder beides.

    Ein Programm kommuniziert über diverse Transport Channels (RS-232, TCP/IP Verbindung, HTTP Verbindung, ...) mit diversen Endpoints (Web-Services, Periphäriegeräte die an einem gemeinsamen Bus hängen oder verschiedenen Funktionsblöcken in diesen Geräten etc.).
    Wichtig ist nur: es gibt mehrere Endpoints die über den selben Channel angesprochen werden müssen.
    Gibt genügend reale Beispiele dafür, musst du mir im Zweifelsfall einfach glauben.
    Und da teilen sich alle Objekte die eine Verbindung zu einem Endpoint darstellen halt einfach die Ownership des Channel Objekts.

    Thread-safe Signals.
    Wenn dir das Problem nicht bekannt ist, dann lies dir die Doku zu Boost.Signals2 durch, da wird es ausführlich erklärt:
    http://www.boost.org/doc/libs/1_56_0/doc/html/signals2/thread-safety.html#idp431602368
    Der Knackpunkt ist: Das Signal muss sicherstellen dass der Slot nicht gelöscht wird während er gerade aufgerufen wird. Das Signal muss also während des Aufrufs "owner" des Slots sein. Gleichzeitig muss aber natürlich der der den Slot connected hat auch "Owner" des Slots sein, da der Slot ja sonst sofort wieder zerstört ("dicsonnected") würde.

    Billig kopierbare immutable Objekte mit Hilfe von shared_ptr<T const> bauen.
    Ist ne "Optimierung", aber eine sehr gute. Ist einfach, effizient und sicher.

    ...

    Ich hatte gehofft, du kommst mir jetzt nicht mit solchen kleinen Details (#2/#3) sondern mit richtigen Szenarien. Bei #1 wuerde ich einfach sicherstellen, dass der Channel laenger lebt.

    Ne, so einfach geht das jetzt nicht.
    Das sind richtige Szenarien.
    Und wie willst du sicherstellen dass der Channel länger lebt. Und wie willst du ohne shared_ptr (oder ähnliche Tools) garantieren dass kein use after free passiert.

    Ich will eine nachvollziehbare Erklärung für alle drei Szenarien.
    Denn alle drei hab' ich schonmal implementiert - die meisten davon mehr als 1x.



  • hustbaer schrieb:

    Ne, so einfach geht das jetzt nicht.
    Das sind richtige Szenarien.
    Und wie willst du sicherstellen dass der Channel länger lebt. Und wie willst du ohne shared_ptr (oder ähnliche Tools) garantieren dass kein use after free passiert.

    Ich will eine nachvollziehbare Erklärung für alle drei Szenarien.
    Denn alle drei hab' ich schonmal implementiert - die meisten davon mehr als 1x.

    struct System
    {
            Channel channel;
            vector<Endpoint> endpoints;
    };
    

    Hmja, muss schon sagen: Auf shared_ptr verzichten ist echt hart.



  • Ja weisste, doof stellen kann ich mich auch.



  • Erklaer doch mal, was an der Loesung nicht passt. Duerfte so ziemlich genau tun, was du erklaert hast. Oder du hast einfach die Anforderungen nicht genau genug erklaert :p



  • Die Endpoint-Connection Objekte werden natürlich an verschiedene andere Objekte übergeben (=Ownership-Transfer), die sich so gar nicht darum scheren wie die Endpoint-Connections implementiert sind, und dass die evtl. einen gemeinsam benutzten Channel haben.
    Genau das ist nämlich vernünftiges API Design.

    Weitere Beispiele:

    Ein Video-Control (GUI-Control, "Widget") das einen "Renderer" braucht um sein Video auf den Schirm zu klatschen. Der Renderer verwendet z.B. D3D oder OpenGL, und du musst 20+ solche Video Controls gleichzeitig unterstützen. In Fenstern die der User nach belieben auf und zumachen kann.
    Die Video-Controls sollten sich also sinnvollerweise einen Renderer teilen, da der Renderer ein nicht ganz billiges Objekt ist (z.B. ein D3DDevice/OGL-Context, ein Shader, mehrere Temp-Texturen).

    Die Videos kommen von Kameras. (Ist jetzt kein eigener Punkt mehr, sondern ein Beispiel für (1)). Die Kameras unterstützen nur eine begrenzte Anzahl an gleichzeitigen "User-Sessions". Man kann also nicht für jeden Scheiss eine eigene aufmachen. Eine User-Session ist nötig um das Livebild zu bekommen und genau so ist eine User-Session nötig um z.B. Bewegungsalarme zu empfangen. Und natürlich um eine Aufzeichnung zu starten, die Kamera zu steuern etc.
    Dummerweise war es dem Entwickler der Kamera-API aber zu doof hier selbst Shared Ownership zu implementieren, und dafür zu sorgen dass z.B. die Livebild-Connection auch die User-Session am Leben hält. Machst du die User-Session zu, "stirbt" damit auch die Livebild-Connection.
    Deine Livebild-Connection Klassen müssen sich also Ownership des User-Session-Handles teilen. Und zwar möglicherweise untereinander (es gibt Fälle wo mehrere Livebild-Connection zur selben Kamera nötig sind, z.B. mit unterschiedlichen Auflösungen/Formaten), als auch mit der User-Session Klasse selbst, die anderorts benötigt wird um eben die Alarme reinzubekommen oder die Kamera zu steuern (bewegen/zoomen/...).
    Und "anderorts" sind einmal Services die im Hintergrund laufen, und dann wieder andere Fenster, die der User auch wieder nach Belieben auf- und zumachen kann.

    Inversion of Control mit nem Service-Locator.
    Da sind verschiedene Services drin registriert, die sich auch untereinander verwenden, und weder der IOC-Container noch der Service-Locator können wissen in welcher Reihenfolge die ganze Show zerstört werden muss damit es kein "use after free" gibt.

    Caches.

    Du hast ne Anwendung in der du irgendwelche "Dokumente" suchen kannst. Ob als headless Service oder GUI ist im Prinzip egal. Der bzw. die User können da Suchanfragen schicken. Die Ergebnisse kommen aus einer Search-Index Klasse die das übernimmt. Dahinter steht keine klassische Datenbank-Connection mit Transaktionen und allem, sondern z.B. ein Lucene Index.
    Hin und wieder muss der Index aktualisiert werden. Das passiert im Hintergrund indem einfach ein neuer Index aufgebaut wird, weil das die eleganteste und auch performanteste Lösung ist. Der neue Index wird parallel zum alten aufgebaut, und wenn er fertig ist soll der alte weggeworfen werden und ab da dann der neue verwendet.
    Die Suchergebnisse die der Index ausspuckt sind aber bloss eine Liste von IDs. Die Daten selbst müssen über die Dokument-ID aus dem Index bezogen werden. Alle Daten über alle Dokumente sofort abzufragen ist nicht vertretbar, da es viel zu lange dauern würde. Der User sieht immer nur ein paar wenige auf einmal so lange er nicht "blättert".
    Die Suchergebnisse müssen aber gültig bleiben während der User drinnen rumstöbert. Die Suchergebnisse müssen also den Index am Leben halten bis er nicht mehr benötigt wird.

    Natürlich wäre es möglich die Suche beim blättern jedes mal zu wiederholen, aber das hat ein paar unerwünschte Nebeneffekte - z.B. dass der User bestimmte Ergebnisse nie sieht, weil sie zwischen der Anfrage für Seite 1 und der für Seite 2 von Seite 2 auf Seite 1 "verrutscht" sind. Und natürlich ist die Performance u.U. deutlich schlechter.
    Aber angenommen damit kannst du leben ... ergibt sich dann gleich das nächste Problem: du kannst den alten Index nicht gegen den neuen austauschen so lange noch irgendwelche Threads IN einer Suchfunktion mit dem alten Index stecken. D.h. du brauchst erst wieder Shared-Ownership um den alten Index so lange am Leben zu halten wie es Threads gibt die darauf zugreifen. ODER du musst über eine zentrale Reader-Writer-Mutex sicherstellen dass keine Reader mehr auf den Index zugreifen während du ihn auswechselst. Das blockiert neue Anfragen so lange bis alle die gerade "in Ausführung" sind fertig gelaufen sind.
    Klar, damit kann man u.U. leben, aber es sind zwei Nachteile die man nicht haben müsste, wenn man einfach akzeptiert dass Shared-Ownership etwas ganz normales ist, und man nicht dagegen ankämpfen sollte.
    Und lustigerweise wirst du selbst damit die Shared-Ownership nicht ganz los - du verschiebst sie bloss in die Reader-Writer-Mutex, so dass du sie selbst "nicht mehr sehen musst". Denn die "Read-Locks" auf die Reader-Writer-Mutex die die Abfrage-Threads halten müssen sind auch nix anderes als Shared-Ownership. (In C++ heisst die Read-Lock Klasse sinnvollerweise sogar std::shared_lock ).

    GUI bzw. 3D Engines. Hier gibt es zwei mir bekannte sinnvolle Varianten. Die eine ist einfach, und verwendet Shared-Ownership für Dinge wie Texturen, Shader, Geometry etc. (die 3D Engines die ich kenne machen das so). Die andere ist deutlich aufwendiger und verwendet Resource-Manager Klassen von denen die nötigen Resourcen jedes mal wenn sie benötigt werden anhand ihrer ID neu angefordert werden.



  • TyRoXx schrieb:

    C# fällt weg, weil Mono unter aller Sau buggy ist. Visual Studio ist einfach lächerlich zurückgeblieben.
    Apple-Müllsprachen fallen natürlich auch weg.
    Java ist scheiße und tot. Es gibt keine vernünftige Entwicklungsumgebung. Soll ich ernsthaft eine Programmiersprache einsetzen, die standardmäßig Malware ("Toolbars") mitliefert?
    Ich benutze C++, weil es im Gegensatz zu anderen Sprachen funktioniert. Es gibt zwei brauchbare Compiler (GCC, Clang), eine brauchbare IDE (QtCreator) und kann zur Not ohne IDE sinnvoll benutzt werden.

    Hast du gesoffen?



  • hustbaer schrieb:

    Inversion of Control mit nem Service-Locator.
    Da sind verschiedene Services drin registriert, die sich auch untereinander verwenden, und weder der IOC-Container noch der Service-Locator können wissen in welcher Reihenfolge die ganze Show zerstört werden muss damit es kein "use after free" gibt.

    Das macht aber auch keinen Spaß ohne einen GC. Außer du beweist immer, daß es keine zirkulären Abhängigkeiten gibt.



  • Ethon schrieb:

    TyRoXx schrieb:

    C# fällt weg, weil Mono unter aller Sau buggy ist. Visual Studio ist einfach lächerlich zurückgeblieben.
    Apple-Müllsprachen fallen natürlich auch weg.
    Java ist scheiße und tot. Es gibt keine vernünftige Entwicklungsumgebung. Soll ich ernsthaft eine Programmiersprache einsetzen, die standardmäßig Malware ("Toolbars") mitliefert?
    Ich benutze C++, weil es im Gegensatz zu anderen Sprachen funktioniert. Es gibt zwei brauchbare Compiler (GCC, Clang), eine brauchbare IDE (QtCreator) und kann zur Not ohne IDE sinnvoll benutzt werden.

    Hast du gesoffen?

    Zumindest den Teil mit der Toolbar kann ich nachvollziehen.



  • Also ne Toolbar mußte ich bei Java nie mitinstallieren. Ich bin wirklich kein Java-Fan, aber fair sollte man schon noch bleiben. 🙄



  • Artchi schrieb:

    Also ne Toolbar mußte ich bei Java nie mitinstallieren. Ich bin wirklich kein Java-Fan, aber fair sollte man schon noch bleiben. 🙄

    Gemeint ist der nicht versteckte Installer für Windows. Es gibt noch andere, die keine Malware installieren.



  • Artchi schrieb:

    Also ne Toolbar mußte ich bei Java nie mitinstallieren. Ich bin wirklich kein Java-Fan, aber fair sollte man schon noch bleiben. 🙄

    Siehe hier. Einfach nur 'ne lächerliche Frechheit dieses Java. Bei einer Runtime drehen die einem Adware an... 👎



  • Ich hatte noch nie eine Java-Toolbar installiert. Und selbst wenn ist Java sehr weit entfernt von tot und es gibt viele und sehr gute Entwicklungsumgebungen.



  • volkard schrieb:

    Du stellst Dich anscheinend als Verteidiger der "C-in-C++"-Programmierer hin,

    Das tue ich nicht. Ich bin keiner von den "C-in-C++"-Programmierern. Ich stecke bei Dir nur fälschlicherweise in so einer Schublade drin.

    volkard schrieb:

    sagst die typischen C-Fehler seien in C++ voll normal.

    Das kann ich auch nicht bestätigen, wenn Du es so formulierst. Ich denke, wir belegen "typische C-Fehler" mit einer anderen Bedeutung. Ich sage, dass bzgl Speichersicherheit C++ nicht wirklich viel besser als C darsteht. Ein bisschen. Nicht viel.

    Zum Beispiel bieten einige Standardbibliotheksimplementierungen zu Debugzwecken so etwas wie "checked iterators" und co an und std::vector<T>::operator[] sowie std::array<T,N>::operator[] werden in so einem Modus auch testen, ob der Index gültig ist. Das ist prima! Deswegen würde ich sagen, C++ ist "ein bisschen besser" bzgl Speichersicherheit, weil es zumindest im Debugmodus bei gescheiter Nutzung der Standardbibliothek zur Laufzeit einige Fehler besser/früher abfangen kann.

    Aber diese Art der Fehlererkennung führt leider dazu, dass die Programme sehr langsam laufen. Das ist zumindest bei g++ der Fall gewesen, last time I checked. Deswegen verwende ich so einen Modus auch nur dann, wenn mir mein C++ Programm mal um die Ohren geflogen ist. Das passiert extrem selten, so selten, dass ich mich nicht mehr daran erinnere, was das für Fehler waren. Aber meine Anwendungsdomäne (number crunching command line tools) ist da auch kein großes Problem in der Hinsicht. Diese Fehlererkennungsmöglichkeiten sind leider auch etwas löchrig. Cooler wär's natürlich, wenn alle Arten von "Referenzen", die ungültig geworden sind, irgendwie abgefangen werden könnten, nicht nur Iteratoren. Und noch cooler wär's, wenn der Großteil dieser Checks schon zur Compilezeit laufen könnten und keine Laufzeitkosten verursachen würden. Dann kann man es sich sogar erlauben, die restlichen Checks drin zu lassen. Denn im Endeffekt wirst Du den C++ Code im "release mode" kompilieren und laufen lassen. Es kommt dann darauf an, ob du bei den vorherigen Testläufen schon alle Fehler abfangen konntest. Ich stelle es mir schwierig vor, Testfälle zu erzeugen, die alle Sicherheitslücken abdecken werden. Denn sonst gäb's nicht so viele Sicherheitslücken in C++ Software.

    Es ist super easy mit einer kleinen Recherche Sicherheitsprobleme in C++ Software zu finden. Ein nicht zu verachtender Teil sind Spechersicherheitsfehler (u.a. use-after-free). Und da ist C++ Software bei, die nicht von Idioten geschrieben wurden, sondern von Leuten wie Du und ich, die "von C++ Ahnung" haben. Statt das anzuerkennen sprichst Du Leuten reflexartig Kompetenzen ab, ohne wirklich einen Überblick über deren Anwendungsdomäne zu haben. Natürlich kommt mir das ignorant und eingebildet vor. Es hilft auch keinem. Es verschlechtert nur den Signal-Rausch-Abstand.

    volkard schrieb:

    Kellerautomat schrieb:

    Es gibt einfach keine Faelle, in denen Exceptions irgendwie sinnvoll sind. Halt, doch. Da war doch was mit Konstruktoren. Hm, vielleicht einfach Konstruktoren entfernen und Funktionen bereitstellen? Koennte klappen. Oder einfach nix im Konstruktor machen, was fehlschlagen koennte. Hey, das klingt doch gut.

    Gefällt mir. Die Sonderrolle der Konstruktoren war mir immer zuwider.

    volkard schrieb:

    Kellerautomat schrieb:

    Andererseits, fuer Sachen wie mathematische Vektoren ist es doch ganz praktisch, wenn ich vector3f(1.f, 2.f, 3.f) schreiben kann. Hmmmm...

    Ja, vector3f(1.f, 2.f, 3.f) ist toll, ist nur Aufrufmagie. Die andere Seite stört mich. Konstruktoren sind static-Methoden, ohne daß ein static da steht. Sie haben einen seltsamen Namen, statt wie in anderen Sprachen new, _construct oder so. Die haben keinen Rückgabetyp, nichtmal void.

    volkard schrieb:

    Ja, auch ein Punkt, den ich doof finde: Objekte wegmoven, ohne daß der Bezeichner verschwindet. Ich hätte gerne

    ding.foo();
    aufruf(move(ding));
    //ding.foo();//gäbe compilerfehler
    

    volkard, das sind super Steilvorlagen von Dir, denen ich nicht entziehen kann: In Rust gibt es diese destruktiven Moves und statt Konstruktoren statische Methoden und Funktionen. Es gibt da auch eine Lösung für das Problem, Member aus Objekten rauszumoven, ohne dass man immer Option benutzen muss (kommt auf den Fall an). Es gibt noch replace und Methoden, die ihr Objekt konsumieren können, so dass der Aufrufer es nicht mehr weiter benutzen kann. In diesen Methoden darf man natürlich das Objekt komplett auseinander nehmen.

    IBV schrieb:

    Scala kennt Null nur von Java, benutzt es in eigenen Libs allerdings nicht. Es wird höchstens None (eher bei Listen. Alle Operationen sind jedoch noch anwendbar.) zurückgegeben oder ein bestimmter Fehlertyp (bei der Verarbeitung von Daten).
    Z. B.:

    request.body.validate[User] match {
      case jsUser: JsSuccess[User] => // Do something
      case error: JsError => // Error Handling
    }
    

    Allgemeine Fehlerbehandlung (diese Konstellation taucht eher selten auf):

    request.body.validate[User] match {
      case jsUser: JsSuccess[User] => // Do something
      case _ => // General Error Handling
    }
    

    Interessant. Das sieht in Rust fast genauso aus. Gibt es in Scala auch so etwas wie das Haskell- do oder das Rust- try! , womit man sich das manuelle match en in vielen Fällen sparen kann?

    type MyResult<T> = Result<T, SendStr>;
    //                           ^^^^^^^ beliebiger Typ für den Fehlerfall
    // SendStr kann String-Objekte oder Verweise auf Stringliterale speichern
    
    fn foo(i: int) -> MyResult<int> {...}
    fn bar(i: int) -> MyResult<int> {...}
    
    fn dings(i: int) -> MyResult<int> {
        try!(foo(i)) * 2 + try!(bar(i+3))
    }
    
    fn main() {
       match dings(23) {
          Err(msg) => println!("Klappte nicht. Fehlermeldung: {}", msg),
          Ok(i) => println!("Das Ergebnis ist {}", i)
       }
    }
    

    wobei try! hier im Normalfall den erwarteten Wert auspackt und im Fehlerfall die Funktion mit dem Fehlerwert per return frühzeitig beendet. Aber es funktioniert bis jetzt nur, wenn der Fehlertyp derselbe ist. Man arbeitet z.Zt. daran, es etwas aufzuboren und benutzerdefinierte Konvertierungen von Fehlertypen zuzulassen. Result bietet auch ein paar nützliche Methoden an, mit denen man sich das eine oder andere match sparen kann.

    In C++ nutze ich Ausnahmen hauptsächlich dazu, Fehlermeldungen bzgl Eingabefehler des Benutzers zur catch -Klausel an main durchzureichen. Wenn man RAII/SBRM konsequent einsetzt, seh' ich da auf Anhieb keine großen Probleme. Ausnahmesicherheit ist für mich aber nicht der einzige Grund für RAII/SBRM.



  • krümelkacker schrieb:

    Interessant. Das sieht in Rust fast genauso aus. Gibt es in Scala auch so etwas wie das Haskell- do oder das Rust- try! , womit man sich das manuelle match en in vielen Fällen sparen kann?

    Nicht, dass ich wüsste, aber das match kann man sich teils auch mit getOrElse sparen.
    Die einfache Variante sieht so aus: http://whileonefork.blogspot.de/2011/05/magic-of-getorelse.html
    Die komplexere mit map so:

    request.session.get(UUID_KEY).map { uuid =>
      // Do something
    }.getOrElse {
      BadRequest("UUID could not be mapped to a captcha solution. Only one try per captcha possible.")
    }
    

    L. G.,
    IBV



  • krümelkacker schrieb:

    Cooler wär's natürlich, wenn alle Arten von "Referenzen", die ungültig geworden sind, irgendwie abgefangen werden könnten, nicht nur Iteratoren.

    Clang hat den Address Sanitizer, der schon einiges mit nur rund 100% Overhead zur Laufzeit finden soll.



  • IBV schrieb:

    Nicht, dass ich wüsste, aber das match kann man sich teils auch mit getOrElse sparen.

    Solche und ähnliche Operationen sind auch praktisch manchmal. (In Rust z.B. unwrap_or , unwrap_or_else )

    TyRoXx schrieb:

    krümelkacker schrieb:

    Cooler wär's natürlich, wenn alle Arten von "Referenzen", die ungültig geworden sind, irgendwie abgefangen werden könnten, nicht nur Iteratoren.

    Clang hat den Address Sanitizer, der schon einiges mit nur rund 100% Overhead zur Laufzeit finden soll.

    Achja, Clang's AdressSanatizer gibt's ja auch noch. Den habe ich noch nicht getestet. Von einem anderen Tool, was die Ausfürhung auch "nur halb so schnell" werden lassen soll, habe ich hier gehört. Den Vortrag kann man sich auch angucken. Ich fand den nicht schlecht.

    Eine Version, die halb so schnell läuft und dabei alle "Speicherfehler" abfangen kann, reicht wahrscheinlich für Testläufe. Und für manche Anwendungen ist das vllt auch schnell genug, um es produktiv einzusetzen, wenn man sich Sorgen um Angreifer macht, die einbrechen wollen.



  • krümelkacker schrieb:

    Interessant. Das sieht in Rust fast genauso aus. Gibt es in Scala auch so etwas wie das Haskell- do oder das Rust- try! , womit man sich das manuelle match en in vielen Fällen sparen kann?

    type MyResult<T> = Result<T, SendStr>;
    //                           ^^^^^^^ beliebiger Typ für den Fehlerfall
    // SendStr kann String-Objekte oder Verweise auf Stringliterale speichern
    
    fn foo(i: int) -> MyResult<int> {...}
    fn bar(i: int) -> MyResult<int> {...}
    
    fn dings(i: int) -> MyResult<int> {
        try!(foo(i)) * 2 + try!(bar(i+3))
    }
    
    fn main() {
       match dings(23) {
          Err(msg) => println!("Klappte nicht. Fehlermeldung: {}", msg),
          Ok(i) => println!("Das Ergebnis ist {}", i)
       }
    }
    

    http://www.scala-lang.org/api/current/#scala.util.Try


Anmelden zum Antworten