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



  • Kellerautomat schrieb:

    Exceptions: Vermutlich eine der schlechtesten Erfindungen in der Geschichte der Programmiersprachen. Es gibt so ziemlich keinen Fall, in dem ich eine Exception verwenden wuerde.

    Ich weiß nicht, seh ich erstmal nicht so eindeutig. Ich verwende zwar auch keine Exceptions, ganz sicher bin ich mir aber nicht, was das betrifft. Woran ich denke, sind Rückgabewerte bei verschachtelten Funktionen. Wenn man eine Klasse C benutzt, die über 20 Funktionsaufrufe irgendwo Funktion X aufruft, und Funktion X will einen Fehler zurückgeben, dann wärs evtl. besser, da eine Exception zu schmeißen, als den Fehlercode durch 20 Funktionen durchzuschleifen. Kann ja sein, dass die sich überhaupt nicht für den Fehler interessieren, aber der Aufrufer der Klasse C könnte damit was anfangen und dann was anderes machen.
    Wie gesagt, hab sowas noch nie benutzt und würde ich auch nicht machen, würde mich aber auch nicht trauen zu behaupten, dass es keinen Sinn macht oder schlecht wäre.



  • 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.



  • Mechanics schrieb:

    Ich weiß nicht, seh ich erstmal nicht so eindeutig. Ich verwende zwar auch keine Exceptions, ganz sicher bin ich mir aber nicht, was das betrifft. Woran ich denke, sind Rückgabewerte bei verschachtelten Funktionen.

    Exceptions sind voellig ueberdesigned mit beliebig schmeissbaren Typen, die per Polymorphie abgefangen werden, wobei man auch wieder aufpassen muss, dass die Exception keine Exception ausloest, dafuer gibt es dann viele Keywords, zusaetzliche Blocke, usw.
    Es wuerde reichen, wenn man eine Exceptionart fuer schwerwiegende Laufzeitfehler anbietet, die dann z.B. einfach nur einen String schmeisst und nicht mehr. Bei lua wird z.B. einfach die Funktion error benutzt, die sowieso schon vom Interpreter fuer Fehler angeboten wird. Dazu gibt es pcall, mit dem eine Funktion gesichert aufruft und das dann ein Error-flag und Fehlermeldung/Rueckgabewert zurueckgibt.
    In Rust ist es aehnlich. Es gibt Makros wie fail! und assert! und wenn sie ausloesen, wird der Task (vergleichbar mit std::asynch) beendet.
    Es gibt aber kaum Gruende, warum man auf klare Programmfehler wie out-of-bound-Errors mit einer Exception reagieren sollte, denn die einzie sinnvolle Reaktion auf solche Fehler ist ein Bugreport an den Entwickler.



  • Wäre für Spieleprogrammierung nichtmal eine Sprache interessant die das Entity-Component Modell anstatt Objektorientierung umsetzt?

    Ich weiß nicht wie man designen könnte und ob man auf Objektorientung verzichten könnte aber irgendwie finde ich die Idee interessant.



  • Marthog schrieb:

    Es wuerde reichen, wenn man eine Exceptionart fuer schwerwiegende Laufzeitfehler anbietet, die dann z.B. einfach nur einen String schmeisst und nicht mehr.

    Naja, ein std::string wäre nett für "exit: Konnte Datei 'C:\autoexec.bat' nicht finden.". Damit der Werfer die eine Meldung wenigstens basteln kann.
    Ein char cont* würde mir aber zu 90% der Fälle reichen. 90% ist zu wenig.
    Ein int(1542="file not found, oder so) ist mir auch zu wenig.
    std::string ist mir aber auch wieder zu beengend, Sprachmittel sollten ohne die std::-lib auskommen, zum Beispiel für Microcontroller.
    Hab geheult, als ich versuchte, C++ auf einen µC zu machen (GCC): Die erste virtuelle Funktion erzeugt die Möglichkeit eines purecalls. Daraufhin hab ich im RAM 30k reservierten Exceptionspeicher und im CODE 350k die <iostream>-lib für eine eventuelle Ausgabe von "purecall".
    Vielleicht sollte man Lamdas werfen. Wo die sich ihren Stringspeicher besorgen, falls nötig, ist doch ihr Problem. Nee, geht auch nicht, Lamdas sind nicht Größenbeschränkt.
    Denke ein char const* reicht. Normalerweise schickt man Adressen von Stringliteralen. Wer mag, kann sich globalen Speicher (256 Byte?) reservieren, da reinschreiben und schicken. Wer mag, kann dort Zeiger auf Lamdas ablegen.

    Daß die std-lib überhaupt Exceptions wirft, ist vielleicht ein Fehler. Außer bad_alloc sehe ich es nicht ganz ein.

    And now for something comletely different:

    iostream ist ugly.
    Also

    int i;
    cin<<i;
    

    ist doch zum Kotzen. Es muss heißen

    int i(cin);
    

    nebst

    class Foo{
       Bar b;
       int i;
       float f;
    public:
       Foo(ifstream& in):
          b(in),i(in),f(in){
       }
    …
    };
    

    Oh, da müssen Exceptions her. Uih, sogar schwächere als nur exit-Exceptions. Daß das Programm gleich exitet, weil ein Formatfehler in einer Datei war? Neee.

    Hab mal für ein MMRPG so einen Lader/Speicherer in C++ gebaut, der genau wie oben aussah. Das Format war JSON-ähnlich, hab bei Stings und Arrays vorher die Länge geschickt. Gegen die alte C-Implementierung hatte ich ca Faktor 180 rausgeholt. Das geht halt toll, weil man sich bei einer Exception-Sprache nur auf das Wesentliche konzentrieren muss. Kein Objekt oder Subobjekt muss sich je drum kümmern. Nur die elementaren Leser werfen bei Fehlformat was. Nur die ganz fette readSaveGame() fängt. Zwischenduch kein if, keine überladenen Returnwerte, keine globalen Fehlerflags, keine std::pair<success,Data>.

    Mist. Struppi hatte recht. Ich will die Möglichkeit haben, zwischenduch abzufangen, und dem Looser die Wahl zu geben, eine andere Datei zu öffnen.

    Hab jetzt Lust, daß gefordert werden würde, daß alle Erben von std::exception nicht größer sind. Mhhm, geht wohl nicht?



  • @Kellerautomat
    Echt jetzt?
    Du willst an jeder Stelle wo etwas erwarteterweise schief gehen kann immer und immer wieder die dämlichen Fehlerchecks wiederholen?
    Function-Chaining adé?

    Wie machst du das dann, alle Factory-Funktionen geben nen unique_ptr zurück der null sein könnte? Und Fehlerwert dann per Output-Parameter? Oder Fehler als Returnwert (int?) und das eigentliche Objekt dann per Output-Parameter?
    Oder bekommt jede Klasse nen Zombie-Zustand?

    Und bei std::string("blub") + std::string(LeiderEinZombie) kommt dann ala Anything + NaN => NaN wieder ein Zombie raus. Oder ...?

    Sorry, aber ne.

    Bei solchen Aussagen frage ich mich immer ernsthaft ob diejenigen die sie bringen schonmal wirklich ein Programm geschrieben haben. Also mehr als puts("Hello world."); .

    Auf Exceptions zu verzichten kann schonmal Sinn machen - WENN man einen guten Grund dafür hat. Aber Exceptions einfach so als Unsinn abzutun ist einfach nur Unsinn.



  • ps:

    Kellerautomat schrieb:

    Es gibt so ziemlich keinen Fall, in dem ich eine Exception verwenden wuerde. (...) File konnte nicht geoeffnet werden? Das ist doch vollkommen erwartet.

    "ist doch vollkommen erwartet" ist aber leider kein Argument.
    Natürlich ist es erwartet. Aber warum sollte man deswegen keine Exceptions verwenden wollen/sollen/dürfen?
    So lange du das nicht erklären kannst het die Feststellung "ist doch vollkommen erwartet" überhaupt keinen Bezug zum Thema.

    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.



  • Ui, da hab ich aber viele Gefuehle verletzt 😃

    Marthog schrieb:

    Exceptions sind voellig ueberdesigned mit beliebig schmeissbaren Typen, die per Polymorphie abgefangen werden, wobei man auch wieder aufpassen muss, dass die Exception keine Exception ausloest, dafuer gibt es dann viele Keywords, zusaetzliche Blocke, usw.

    Ich will mal diesen Satz hervorheben. Das ist ziemlich genau, was ich mir denke.

    volkard schrieb:

    Daß die std-lib überhaupt Exceptions wirft, ist vielleicht ein Fehler. Außer bad_alloc sehe ich es nicht ganz ein.

    +1

    volkard schrieb:

    And now for something comletely different:

    iostream ist ugly.
    Also

    int i;
    cin<<i;
    

    ist doch zum Kotzen. Es muss heißen

    int i(cin);
    

    nebst

    class Foo{
       Bar b;
       int i;
       float f;
    public:
       Foo(ifstream& in):
          b(in),i(in),f(in){
       }
    …
    };
    

    Neee, so doch nich. Viel zu wenig generisch. Wir brauchen ein (automatisch generiertes) fmap.

    struct Foo
    {
            int i;
            double d;
    };
    
    // vom compiler generiert
    template <typename F>
    void fmap(Foo& foo, F&& f)
    {
            f(foo.i);
            f(foo.d);
    }
    
    // alles rekursiv abarbeiten
    struct Read
    {
            Read(istream& is)
                    : is_(&is)
            {}
    
            void operator () (int& i)
            {
                    *is_ >> i;
            }
    
            void operator () (double& d)
            {
                    *is_ >> d;
            }
    
            // usw. ...
    
            template <typename T>
            void operator () (T& obj)
            {
                    fmap(obj, *this);
            }
    
    private:
            istream* is_;
    };
    
    template <typename T>
    void read(istream& is, T& obj)
    {
            fmap(obj, Read(is));
    }
    
    read(is, obj);
    

    Ach Mist, jetzt sind wir ja wieder, wo wir vorher waren. Also noch ein kleines Helferlein.

    template <typename T>
    T readFromStream(istream& is)
    {
            T x;
            read(is, x);
            return x;
    }
    
    auto x = readFromStream<Foo>(cin);
    

    Na geht doch. Und ich muss keine istream Konstruktoren mehr definieren. Und fmap kann ich auch fuer anderes verwenden, z.B. einen operator < und Konsorten. Zwei Fliegen mit einer Klatsche.

    volkard schrieb:

    Oh, da müssen Exceptions her. Uih, sogar schwächere als nur exit-Exceptions. Daß das Programm gleich exitet, weil ein Formatfehler in einer Datei war? Neee.

    Hab mal für ein MMRPG so einen Lader/Speicherer in C++ gebaut, der genau wie oben aussah. Das Format war JSON-ähnlich, hab bei Stings und Arrays vorher die Länge geschickt. Gegen die alte C-Implementierung hatte ich ca Faktor 180 rausgeholt. Das geht halt toll, weil man sich bei einer Exception-Sprache nur auf das Wesentliche konzentrieren muss. Kein Objekt oder Subobjekt muss sich je drum kümmern. Nur die elementaren Leser werfen bei Fehlformat was. Nur die ganz fette readSaveGame() fängt. Zwischenduch kein if, keine überladenen Returnwerte, keine globalen Fehlerflags, keine std::pair<success,Data>.

    Mist. Struppi hatte recht. Ich will die Möglichkeit haben, zwischenduch abzufangen, und dem Looser die Wahl zu geben, eine andere Datei zu öffnen.

    Hab jetzt Lust, daß gefordert werden würde, daß alle Erben von std::exception nicht größer sind. Mhhm, geht wohl nicht?

    Ok, ich gestehe. Fuer sowas hab ich auch schon Exceptions verwendet. Aber vielleicht koennte man etwas bauen, das aehnlich funktioniert, ohne gleich Exceptions zu brauchen. Ein Sprung zum naechsten Error-Handler oder so?

    hustbaer schrieb:

    @Kellerautomat
    Echt jetzt?
    Du willst an jeder Stelle wo etwas erwarteterweise schief gehen kann immer und immer wieder die dämlichen Fehlerchecks wiederholen?
    Function-Chaining adé?

    Wie machst du das dann, alle Factory-Funktionen geben nen unique_ptr zurück der null sein könnte? Und Fehlerwert dann per Output-Parameter? Oder Fehler als Returnwert (int?) und das eigentliche Objekt dann per Output-Parameter?
    Oder bekommt jede Klasse nen Zombie-Zustand?

    Und bei std::string("blub") + std::string(LeiderEinZombie) kommt dann ala Anything + NaN => NaN wieder ein Zombie raus. Oder ...?

    Sorry, aber ne.

    Bei solchen Aussagen frage ich mich immer ernsthaft ob diejenigen die sie bringen schonmal wirklich ein Programm geschrieben haben. Also mehr als puts("Hello world."); .

    Guck dir mal an, wie sowas in Haskell gemacht wird. Das ist elegant. Stichwort Maybe/Either.
    Zu deinem std::string Beispiel sag ich einfach mal nix, da du offensichtlich meine Posts garnicht richtig gelesen hast. Du findest die Antwort dort.

    hustbaer schrieb:

    Auf Exceptions zu verzichten kann schonmal Sinn machen - WENN man einen guten Grund dafür hat. Aber Exceptions einfach so als Unsinn abzutun ist einfach nur Unsinn.

    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.

    hustbaer schrieb:

    ps:

    Kellerautomat schrieb:

    Es gibt so ziemlich keinen Fall, in dem ich eine Exception verwenden wuerde. (...) File konnte nicht geoeffnet werden? Das ist doch vollkommen erwartet.

    "ist doch vollkommen erwartet" ist aber leider kein Argument.
    Natürlich ist es erwartet. Aber warum sollte man deswegen keine Exceptions verwenden wollen/sollen/dürfen?
    So lange du das nicht erklären kannst het die Feststellung "ist doch vollkommen erwartet" überhaupt keinen Bezug zum Thema.

    Uebersetz mal "Exception" auf Deutsch.

    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.

    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.



  • Ich finde Exceptions vor allem hässlich, aber nicht überflüssig. In C-Manier "if (return-Val == Fehler-Code)" finde ich schlimmer.
    Was ich schlimmer finde, ist null. Hätte nie erfunden werden dürfen. Darauf zu testen ist hässlich und falls man es vergisst, kann das Programm abstürzen.
    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
    }
    

    L. G.,
    IBV



  • Kellerautomat schrieb:

    template <typename T>
    T readFromStream(istream& is)
    {
            T x;
            read(is, x);
            return x;
    }
    

    Nee. Serialisierter, die Standdardkonstruktoren verlangen, sind für mich indiskutabel.



  • volkard schrieb:

    Nee. Serialisierter, die Standdardkonstruktoren verlangen, sind für mich indiskutabel.

    Gib mal ein Beispiel von einem Typen, bei dem es Sinn macht, keinen Standardkonstruktor zu haben.



  • Kellerautomat schrieb:

    volkard schrieb:

    Nee. Serialisierter, die Standdardkonstruktoren verlangen, sind für mich indiskutabel.

    Gib mal ein Beispiel von einem Typen, bei dem es Sinn macht, keinen Standardkonstruktor zu haben.

    Umgekehrt! Wo braucht man ihn? Was man nicht braucht, soll man nicht in die Schnittstelle tun.
    Also mal alle Monstertypen im Rollenspiel sollen keinen haben. Wäre doch totaler Unfug. Streams sollen keinen haben. Ich könnte auch damit leben, daß Zahlen und Strings keinen haben. Manche rohe Typen dürften, um andere Meckerstellen der Sprache zu umgehen, sowas erlauben wie

    int i=uninitialized;//neues schlüsselwort. ein =0 wäre zu lahm und 
      //die magic number wäre inhaltlich äußerst fragwürdig. 
    do
      i=rand()%100;//meckerstelle: i kann hier nicht erzeigt werden, weil…
    while(schonGezogen(i));//…i hier gebraucht wird
    return i;
    

    .
    Zusammen mit dem RAII-Dingens mache ich eigentlich meine Variablen so lokal, daß ich sie stets erst anlege, wenn sie schon sinnvoll initialisiert werden können.
    Ok, man braucht dafür, um kein schlechtes Gewissen zu bekommen, auch mal emplace_back und einen vector mit fixer Maximalgröße, der nicht im Freispeicher wohnt.



  • volkard schrieb:

    Umgekehrt! Wo braucht man ihn? Was man nicht braucht, soll man nicht in die Schnittstelle tun.

    Alles, was irgendwie in Container soll. Alles, was irgendwie Movable ist bzw. einen "empty" Zustand hat.

    volkard schrieb:

    Also mal alle Monstertypen im Rollenspiel sollen keinen haben. Wäre doch totaler Unfug.

    Noe, warum. Wenn du ein Monster default-Konstrutierst, haste sowas wie ein moved-from Monster. Das kriegst du spaetestens wenn du einen Move-Konstrutkor / operator = verwendest. Also kannst du auch gleich einen Defaultkonstruktor bereitstellen.

    volkard schrieb:

    Streams sollen keinen haben.

    Genau wie mit den Monstern.

    volkard schrieb:

    Ich könnte auch damit leben, daß Zahlen und Strings keinen haben. Manche rohe Typen dürften, um andere Meckerstellen der Sprache zu umgehen, sowas erlauben wie

    int i=uninitialized;//neues schlüsselwort. ein =0 wäre zu lahm und 
      //die magic number wäre inhaltlich äußerst fragwürdig. 
    do
      i=rand()%100;//meckerstelle: i kann hier nicht erzeigt werden, weil…
    while(schonGezogen(i));//…i hier gebraucht wird
    return i;
    

    .
    Zusammen mit dem RAII-Dingens mache ich eigentlich meine Variablen so lokal, daß ich sie stets erst anlege, wenn sie schon sinnvoll initialisiert werden können.
    Ok, man braucht dafür, um kein schlechtes Gewissen zu bekommen, auch mal emplace_back und einen vector mit fixer Maximalgröße, der nicht im Freispeicher wohnt.

    uninitialized finde ich auch gut. Hat aber mit Defaultkonstruktoren wenig zu tun.



  • Alle Deine Argument für Standardkonstruktoren sind also nicht inhaltlicher Art im Sinne der Anwendungslogik, sondern programmiertechnische Zugeständnisse.

    Kellerautomat schrieb:

    volkard schrieb:

    Umgekehrt! Wo braucht man ihn? Was man nicht braucht, soll man nicht in die Schnittstelle tun.

    Alles, was irgendwie in Container soll. Alles, was irgendwie Movable ist bzw. einen "empty" Zustand hat.

    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
    

    aufruf müßte die unbedingte Destuktionsverantwortung haben. Normalerweise wird an einen Subaufruf weitergemovt. Im Falle eines vorhergehenden return/throw muss ding halt wie eigene lokale Variablen destruiert werden. Für solche Sachen werfe ich Augen auf Rust und Konsorten.

    Kellerautomat schrieb:

    uninitialized finde ich auch gut. Hat aber mit Defaultkonstruktoren wenig zu tun.

    Bei ints auf dem Stack schon.



  • Kellerautomat schrieb:

    Ok, ich gestehe. Fuer sowas hab ich auch schon Exceptions verwendet. Aber vielleicht koennte man etwas bauen, das aehnlich funktioniert, ohne gleich Exceptions zu brauchen. Ein Sprung zum naechsten Error-Handler oder so?
    [...]
    Uebersetz mal "Exception" auf Deutsch.

    Das ist ja lustig. Und wenn die Exceptions anders hießen, würdest du dich evtl. überwinden, sie zu verwenden, anstatt sie von Hand nachzubauen?



  • 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
    

    Hmm:

    if(random() % 2)
        foo(move(bar));
    bar.foobar();
    


  • Ethon schrieb:

    Hmm:

    if(random() % 2)
        foo(move(bar));
    bar.foobar();
    

    Wie in Rust: Sobald es auch nur einen Weg gibt, auf dem ein Objekt uninitialisiert ist oder weggemoved wurde, gibt es einen Compilerfehler. Der Programmierer hat dann sicherzustellen, dass es bei allen Wegen gueltig ist. In deinem Beispiel koennte man z.B. ein else einbauen oder ein neues Objekt erzeugen.

    EDIT: In C++ sind alle Methodenaufrufe von Standardklassen ausser die pointer undefiniertes Verhalten, wenn das Objekt weggemoved wurde und nicht neu zugewiesen wurde.



  • Ethon schrieb:

    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
    

    Hmm:

    if(random() % 2)
        foo(move(bar));
    bar.foobar();
    

    Verboten. Auch nach geiftem move ist der Bezeichner weg. Wer sowas machen will, darf smartpointers/optional etc nehmen.



  • Und was machst du dann mit Membern?



  • Kellerautomat schrieb:

    Und was machst du dann mit Membern?

    Auch wegwerfen, wie üblich.


Anmelden zum Antworten