Interface für generischen Typ



  • Tachyon schrieb:

    Ne, das geht genau so, wie der Aufruf von überladenen Funktionen.

    Nein, Überladung wird zur Kompilierzeit aufgelöst.

    Welcher catch -Block aufgerufen wird, ist erst zur Laufzeit bekannt.



  • Nexus schrieb:

    Tachyon schrieb:

    Ne, das geht genau so, wie der Aufruf von überladenen Funktionen.

    Nein, Überladung wird zur Kompilierzeit aufgelöst.

    Welcher catch -Block aufgerufen wird, ist erst zur Laufzeit bekannt.

    Also mit den Compilern mit denen ich das hier gerade testen kann geht es auf jeden Fall ohne RTTI, und der Standard stimmt mir eigentlich auch zu. Ich meine allerdings, mich daran erinnern zu können, dass VS6 damit nicht klar gekommen ist.



  • Einfaches Beispiel:

    void Function()
    {
        if (rand() % 2 == 0)
            throw int(3);
        else
            throw double(7.2);
    }
    
    int main()
    {
       try
       {
            Function();
       }
       catch(int)
       {
            ...
       }
       catch(double)
       {
            ...
       }
    }
    

    Wie soll hier zur Kompilierzeit bestimmt werden, welcher catch -Block zum Zug kommt?



  • Genauso wie bei Polymophie und Überladung: Über eine interne Dispatch-Tabelle. Es ist doch alles zur Compilezeit bekannt. Sowohl die Handler, als auch die Exception als auch die throw-Operatoren und deren Positionen. Was soll man da mit RTTI rumdoktorn müssen?



  • Na indem der Compiler nur den geraden Zufall zuläßt. 😉
    Da muss man halt etwas kreativ sein. 😃



  • hustbaer schrieb:

    RTTI hat nur dort Performance-Overhead, wo du es auch verwendest. "Getraced" wird da nix. Normalerweise geht das über den VTable-Zeiger, und den muss es sowieso geben, sobald man virtuelle Funktionen verwendet. Da virtuelle Funktionen Voraussetzung für RTTI sind -> kein Overhead. Ausser eben dort wo man Dinge wie typeid() oder dynamic_cast auch wirklich verwendet.

    Ok super, damit ist der Fall klar. Bei allfälligen Performanceproblemen kann man sich ja immer noch mit einer eigenen, simpleren und allenfalls performanteren Lösung rumschlagen.

    seldon schrieb:

    Das Problem ist, dass du auf verschiedene Typen keinen identischen Zugriff bekommst, sofern diese nicht von der selben Basisklasse erben, die das Interface bereitstellt - und in diesem Fall ist die ganze Apparatur eh unnötig.

    Jein, ich habe mich vielleicht nicht ganz klar ausgedrückt. Der Zugriff ist natürlich nicht identisch, da man jeweils den Typen für die Templatefunktion angeben muss ( as<int>() ), allerdings kann ich so beispielsweise sehr angenehm und eben (fast) einheitlich Daten in einen assoziativen Container legen, also beispielsweise etwas wie

    UserSettings["FileName"].as<std::string>()="Meine Testdatei";
    UserSettings["GroupModeActivated"].as<bool>()=false;
    UserSettings["MaxNrOfEntries"].as<int>()=20;
    

    seldon schrieb:

    Überleg dir, wie der benutzende Code am Ende aussehen soll bzw. muss - ich habe Schwierigkeiten, mir ein anderes Vorgehen vorzustellen als "Versuch's mit dem Typ. Funzt nicht? Probier's mit dem nächsten.", und das ist alles andere als sauber.(...)

    Jetzt seh ich auch deinen Einwand: Natürlich wäre dies eine ziemlich bescheuerte Art der Benützung eines solchen Containers. Der Gedanke hinter der Exception ist eigentlich, dass diese überhaupt nicht auftreten sollte wenn der Programmierer keinen Fehler baut - aber man muss natürlich auf die Eventualität vorbereitet sein.
    Ich kann aber in meinem Programm Datenblöcke haben, die logisch zusammengehörende Informationen repräsentieren, allerdings aber nicht vom gleichen Typen sind. Und da dies mehrfach in Variationen vorkommt, kann ich mit der generischen Variante eine anpassungsfähige und vielfältig einsetzbare Implementation bauen, die immer wieder verwendet werden kann und mit Templates eben nicht möglich ist (Weil die Anzahl unterschiedlicher Typen auch variiert). Ich muss dann also nicht immer wieder eine neue Klasse bauen für jede eigene spezielle Instanz, sondern kann die bestehende Klasse verwenden und muss nur den Zugriff korrekt implementieren.
    Allgemein würde ich sagen gibts zwei Arten von solchen Informationen und auch zwei Arten sie zu implementieren: Zum einen objektspezifische Informationen, wie beispielsweise Name, eine Id, Dinge, die man dann wohl als Membervariablen der jeweiligen Klasse realisieren würde. Und zum andern das ganze Programm betreffende Einstellungen, die immer mal wieder irgendwo benötigt werden von unterschiedlichen Klassen. Gerade auch wenn man die ganzen Einstellungen dann beispielsweise als xml jeweils sichert, stellt sich dann schon die Frage, wie sinnvoll es ist, das ganze zerstückelt in Membervariablenform zu implementieren (wo dann jede Zuweisung separat notiert werden muss), oder ob man nicht besser bestimmte Dinge zentral verwaltet und dort die Einstellungen über eine ganz praktische Schleife reinladen kann.
    Ob man dann jeweils zu Programmstart alle Einstellungen lädt oder sich nur spezifisch diejenigen holt, die vorzu gerade benötigt werden, ist dann natürlich noch eine andere Frage.

    Ich habe vor, sowas beispielsweise für mein Exceptionhandling zu verwenden: Ich instanziere meine Exceptions über Ids wie "FehlerIdBlah" und hab dann zu der Id gehörende Felder unterschiedlichen Typs wie z.B. Description, UserBenachrichtigen, Fehler loggen oder nicht, etc...
    Für sowas ist die generische Klasse dann natürlich sehr praktisch.. Aber der Zugriff steht zur Compiletime schon fest, das Programm muss nicht noch raussuchen, mit welchem Typen er stattzufinden hat.



  • SEH bittet dir an solche Information im Stack abzulegen. Im Endeffekt kann man sich lange Streiten, solange man nicht genau weißt, nach welchen Modell die Implementation arbeitet.



  • Tachyon schrieb:

    Genauso wie bei Polymophie und Überladung: Über eine interne Dispatch-Tabelle. Es ist doch alles zur Compilezeit bekannt. Sowohl die Handler, als auch die Exception als auch die throw-Operatoren und deren Positionen. Was soll man da mit RTTI rumdoktorn müssen?

    Die throws in der Funktion können aber zur Kompilierzeit nicht "fest" mit einem catch verdrahtet werden, weil die Funktionen ja in verschiedenen Kontexten aufgerufen werden können. Die einzige Möglichkeit wäre dann, die Funktion in allen verschiedenen Varianten abzuspeichern mit den jeweils fest verdrahteten throw/catches und jeweils im Aufruf die richtige zu verlinken - Dann hättest du aber massenweise redundanten Maschinencode. Oder du müsstest eine Information mitführen, woher der Aufruf kam und entsprechend in einer jump-table den richtigen catch Block auswählen. Dann hättest du aber auch wieder eine Runtime-Information mitgeführt..


  • Mod

    Tachyon schrieb:

    als auch die Exception als auch die throw-Operatoren und deren Positionen.

    Woher?

    extern "weird" void opaque();
    
    void so_something()
    {
       try
       {
           opaque();
       }
       catch ( foo )
    ...
    

    Hier weiß der Compiler gar nichts.



  • Kleiner Test-Code (getestst auf MSVC 9 und gcc 4.5)

    //a.hpp
    struct A{ };
    
    struct B{ };
    
    void f();
    
    //a.cpp
    void f()
    {
        if((std::rand() % 2) == 0)
        {
            throw A();
        }
        else
        {
            throw B();
        }
    }
    
    //main.cpp
    struct E { virtual ~E(){} };
    
    struct F : E { };
    
    int main()
    {
        std::srand(std::time(0));
        for(std::size_t n = 0; n != 10; ++n)
        {
            try
            {
                f();
            }
            catch(A & e) //geht ohne RTTI
            {
                std::cout << "Was A\n";
            }
            catch(B & e) //geht ohne RTTI
            {
                std::cout << "Was B\n";
            }
        }
        A a;
        std::cout << typeid(a).name() << '\n'; //geht ohne RTTI
        F f;
        E &k = f;
        std::cout << typeid(k).name() << '\n'; //geht NICHT ohne RTTI
    
    }
    


  • camper schrieb:

    Tachyon schrieb:

    als auch die Exception als auch die throw-Operatoren und deren Positionen.

    Woher?

    extern "weird" void opaque();
    
    void so_something()
    {
       try
       {
           opaque();
       }
       catch ( foo )
    ...
    

    Hier weiß der Compiler gar nichts.

    Der Exception-Typ muss beidseitig voll bekannt sein. Der Compiler kann entsprechende Linker-Infos in das Compilat einbauen, über die sich das Regeln lässt.



  • Zeus schrieb:

    Vorsicht! Jetzt kommen die Protype-OOP-Jungs und verhauen dich *gg*

    Ja, das würden sie wahrscheinlich - die machen ja den ganzen Tag nichts anderes als diesen Wahnsinn. Ich sehe ihren Kram etwa so wie Perl, Shell oder PHP - für Kleinkram durchaus brauchbar, aber auch, wenn es möglich ist, größere Projekte darin zu entwickeln, lässt man es besser bleiben. Typen zur Laufzeit zusammenbauen, ha! Auf welche Schnittstellen soll man sich denn da noch verlassen können?

    ccquestions: Ich sehe immer noch keinen plausiblen Grund, die Typverarbeitung in die Laufzeit zu verlegen - du musst ja doch zur Compilezeit wissen, welchen Typ du brauchst, und auch, welche du speichern kannst (um sie nämlich speichern zu können). Du könntest sogar das Interface, das du beschreibst, implementieren, wenn UserSettings sich intern mehrere Maps mit verschiedenen Werttypen vorhält, und es spart Laufzeit, weil weniger Schlüsselwerte durchsucht werden müssen. Wobei ich eigentlich kein Problem mit

    UserSettings.StringOptions["FileName"] = "Meine Testdatei";
    

    sehe.

    Trotzdem halte ich es immer noch für sinnvoller, die Einstellungen gleich zu verarbeiten. Du musst ja nicht nur den Typen von GroupModeActivated zur Compilezeit kennen, sondern auch seinen Namen, jedenfalls, sofern du nicht gerade eine Art Framework-Framework baust.

    Exception-Handling wird übrigens heutzutage üblicherweise so betrieben, dass Programmzustände (ähnlich wie mit setjmp gespeichert, wobei stack unwinding das Ganze etwas komplizierter macht) in Dispatch-Tabellen, wie Tachyon sie benennt, verknüpft werden mit Bereichen des Befehlszählers. Aus diesem lässt sich ziemlich offensichtlich eindeutig bestimmen, was gerade geworfen wird.



  • @seldon
    Du verwechselt zwei unterschiedliche Konzepte im Typensystem.
    - class vs protypen
    - statisch vs dynamisch



  • Achso: Am Besten sagt man, danke ich, dass sich RTTI eigentlich nicht abschalten lässt. Zumindest finde ich nichts im Standard, was das vorsieht. Vielleicht bei Free-Standing-Implementierungen. Daher wird es wohl abhängig von der Implementierung sein, was ohne RTTI geht, und was nicht.



  • seldon schrieb:

    ccquestions: Ich sehe immer noch keinen plausiblen Grund, die Typverarbeitung in die Laufzeit zu verlegen - du musst ja doch zur Compilezeit wissen, welchen Typ du brauchst, und auch, welche du speichern kannst (um sie nämlich speichern zu können).

    Ja. Wenn ich nur eine solche Instanz habe, könnte man das auch ganz konventionell realisieren. Aber wenn ich verschiedene sehr ähnliche aber dennoch nicht ausreichend ähnliche "Informationstabellen" habe, dann müsste ich dann für jede dieser Tabellen eine eigene Klasse bauen, obwohl praktisch gleich.

    seldon schrieb:

    Du könntest sogar das Interface, das du beschreibst, implementieren, wenn UserSettings sich intern mehrere Maps mit verschiedenen Werttypen vorhält, und es spart Laufzeit, weil weniger Schlüsselwerte durchsucht werden müssen. Wobei ich eigentlich kein Problem mit

    UserSettings.StringOptions["FileName"] = "Meine Testdatei";
    

    sehe.

    Meine ursprüngliche Realisierung hat ziemlich genau so ausgesehen, ja. Sie ist aber natürlich um einiges weniger flexibel. Es werden nicht derart viele Schlüsselwerte sein, dass eine Auftrennung einen merklichen Performancegewinn bewirken würde.

    seldon schrieb:

    sofern du nicht gerade eine Art Framework-Framework baust.

    Dies ist tatsächlich der Fall. 🙂 Es handelt sich um ein Framework, das auch durch reine Änderung von xml-Konfigurationsdateien und das dazufügen von solchen stark vom Benutzer wird beeinflusst werden können. Beispielsweise neue xml-Definitionsdatei->Neue im Programm zur Verfügung stehende Objektklasse.



  • Zeus schrieb:

    @seldon
    Du verwechselt zwei unterschiedliche Konzepte im Typensystem.
    - class vs protypen
    - statisch vs dynamisch

    Hmm...okay, belehr mich. Ich hatte das jetzt so im Kopf, dass prototypbasierte Sprachen als Typ im Grunde nur Objekte generell kennen und verfügbare Schnittstellen sich zur Laufzeit daraus ergeben, welcher Prototyp geklont bzw. erweitert wurde. Auf die Weise hat man keine Möglichkeit, von beispielsweise seinen Funktionsparametern bestimmte Interfaces zu verlangen und muss entweder darauf hoffen, dass man da nie etwas Falsches bekommt oder sich mit massenhaft NoSuchMethodErrors (oder wie auch immer das gerade heißen mag) herumschlagen, damit man den Zustand seiner Anwendung nicht korrumpiert (lass so was mal in die Nähe einer Datenbank!). Praktisch impliziert für mein Verständnis ein prototypisches Typsystem auch ein dynamisches.

    Aber wenn ich damit falsch liege - hast du eine Webseite oder so zur Hand, wo ich mein Missverständnis korrigieren kann? Ich will ja nicht ewig dumm bleiben.

    @ccquestions: Hm. Also kann ein Benutzer quasi zur Laufzeit neue Typen dazuladen, die von der Anwendung als Werttypen für die Konfiguration benutzt werden? Ich mal mir da jetzt was mit Plugin-Systemen aus, und mein Instinkt wäre es, bei solchen Typen auf einer gemeinsamen Basisklasse zu beharren, die die Konfigurationskomponente kennt, aber so richtig klar ist mir noch nicht, was du da eigentlich baust. Sieht auch so aus, als würde es den Rahmen des Forums sprengen, also unterwerfe ich mich deiner besseren Kenntnis des Sachverhalts.



  • @Tachyon:
    Um herauszubekommen ob ein Catch-Handler passt, muss geprüft werden ob die Typen kompatibel sind.
    Da es nicht möglich ist, das in allen Fällen zur Compile-Zeit zu machen, muss man es (zumindest in manchen Fällen) zur Laufzeit machen.
    Man benötigt also zur Laufzeit Informationen über die Typen.
    Laufzeit. Runtime.
    Informationen über die Typen. Type Information.

    Runtime Type Information.

    RTTI.

    Nen?



  • Tachyon schrieb:

    camper schrieb:

    Tachyon schrieb:

    als auch die Exception als auch die throw-Operatoren und deren Positionen.

    Woher?

    extern "weird" void opaque();
    
    void so_something()
    {
       try
       {
           opaque();
       }
       catch ( foo )
    ...
    

    Hier weiß der Compiler gar nichts.

    Der Exception-Typ muss beidseitig voll bekannt sein. Der Compiler kann entsprechende Linker-Infos in das Compilat einbauen, über die sich das Regeln lässt.

    Der Exception Typ muss in der TU wo der Catch-Handler definiert ist überhaupt nicht bekannt sein, nur der zu-fangende Typ. Und für den reicht soweit ich weiss auch ne fwd. decl., wenn man bloss (by Reference) fangen will, ohne mit dem gefangenen Objekt dann noch was anzustellen.



  • Der Gedanke hinter der Exception ist eigentlich, dass diese überhaupt nicht auftreten sollte wenn der Programmierer keinen Fehler baut - aber man muss natürlich auf die Eventualität vorbereitet sein.

    Ich denke das ist der falsche Ansatz. Zwischen Exceptions und Programmierfehlern sollte mMn unterschieden werden. Programmierfehler kann man auch schön mit einem assert abfangen, Exceptions sollten hingegen für wirkliche Ausnahmesituationen verwendet werden, also Situationen, deren Auftreten du nicht beeinflussen kannst (Datei kann nicht geöffnet werden, kein Festplattenspeicher mehr, etc).

    My 2 cents.



  • seldon schrieb:

    Auf die Weise hat man keine Möglichkeit, von beispielsweise seinen Funktionsparametern bestimmte Interfaces zu verlangen und muss entweder darauf hoffen, dass man da nie etwas Falsches bekommt oder sich mit massenhaft NoSuchMethodErrors (oder wie auch immer das gerade heißen mag) herumschlagen,
    ...
    Praktisch impliziert für mein Verständnis ein prototypisches Typsystem auch ein dynamisches.

    Das hat nichts mit Protoytpen zu tun. Du kannst zB in python ganz ohne Prototypen ein dynamic typing haben.

    Und das dynamische Typsysteme durchaus auch in der Praxis bei großen Anwendungen funktionieren ist erwiesen.

    Und in der Theorie braucht man für prototyping auch kein dynamisches Typsystem. Siehe zB lisaac.


Anmelden zum Antworten