Auslagern von plattformspezifischem Code



  • Wie schon gesagt: Es geht nicht konkret um genau eine Variable. Es geht darum, dass ich irgendetwas Plattformspezifisches da drin haben kann.

    Beispiel:
    Wenn ich ein Spiel mit DirectX schreibe und die Technik zum Erstellen von Sprites in einer Klasse wrappe, muss ich da irgendein DirectDraw-Handle für das Bild speichern und eine X- und Y-Position.
    Wenn ich das ganze dann gleichermaßen auch mit SFML implementieren will, dann brauche ich keine eigene X- und Y-Variable, weil es in der SFML eine spezielle Sprite-Klasse mit solchen Angaben gibt und ich dann einfach ein Objekt dieser Klasse in meiner eigenen Klasse wrappe.
    Und wenn ich GDI benutze, brauche ich ein HBITMAP, eine X-Variable, eine Y-Variable und ein paar kleine Hilfsfunktionen, die mir mal eben einen HDC aus einem HBITMAP erzeugen und was es sonst noch so gibt.



  • Waper schrieb:

    Wie schon gesagt: Es geht nicht konkret um genau eine Variable. Es geht darum, dass ich irgendetwas Plattformspezifisches da drin haben kann.

    Beispiel:
    Wenn ich ein Spiel mit DirectX schreibe und die Technik zum Erstellen von Sprites in einer Klasse wrappe, muss ich da irgendein DirectDraw-Handle für das Bild speichern und eine X- und Y-Position.
    Wenn ich das ganze dann gleichermaßen auch mit SFML implementieren will, dann brauche ich keine eigene X- und Y-Variable, weil es in der SFML eine spezielle Sprite-Klasse mit solchen Angaben gibt und ich dann einfach ein Objekt dieser Klasse in meiner eigenen Klasse wrappe.
    Und wenn ich GDI benutze, brauche ich ein HBITMAP, eine X-Variable, eine Y-Variable und ein paar kleine Hilfsfunktionen, die mir mal eben einen HDC aus einem HBITMAP erzeugen und was es sonst noch so gibt.

    Dann baust Du Dir doch eine Klasse Sprite oder typedefst Typen und bietest passende Funktionen an, die auf allen Plattformen gleich zu bedienen sind.
    Und inkludierst dann im Spiel Deine eigene "os.hpp" oder wieauchimmer sie heißen mag.
    Und hast im Spiel selber kein #ifdef mehr drin.



  • Sag mal, ist das so schwer zu verstehen?

    In GDI gibt es vielleicht ein CreateHDCFromHBITMAP. In allen anderen Technologien wird sowas nichtmal ansatzweise gebraucht.
    DirectX braucht ein GetVisibleRectangeFromSprite, weil DirectDraw abstürzt, wenn man versucht, ein komplettes Sprite so zu blitten, dass es teilweise außerhalb der Ziel-Oberfläche liegt, weshalb man vorher manuell den sichtbaren Teil des Sprites berechnen muss und nur den blitten darf. GDI dagegen kann Bilder auf diese Weise automatisch blitten. Und SFML wahrscheinlich auch.

    Ich weiß doch überhaupt nicht, welche internen Hilfsfunktionen ich so brauchen könnte, die jetzt mit dem Sprite-Objekt gar nicht direkt zu tun haben, sondern mir nur Tipparbeit ersparen. Was soll also das Gerede von wegen

    bietest passende Funktionen an, die auf allen Plattformen gleich zu bedienen sind

    ?



  • Dieser Thread wurde von Moderator/in volkard aus dem Forum C++ (auch C++0x und C++11) in das Forum Spiele-/Grafikprogrammierung verschoben.

    Im Zweifelsfall bitte auch folgende Hinweise beachten:
    C/C++ Forum :: FAQ - Sonstiges :: Wohin mit meiner Frage?

    Dieses Posting wurde automatisch erzeugt.



  • Oh Mann. Jetzt habt Ihr das ganze nach Spiele- und Grafikprogrammierung gepackt, bloß weil mein Anschauungsbeispiel sich damit beschäftigte, obwohl die Ausgangsfrage (Wie implementiere ich plattformabhängigen Code, ohne bestehende Quellcodedateien zuzumüllen?) weiterhin völlig themenneutral ist? Wenn also jemand das nächste Mal eine Frage zur math.h stellt, packt Ihr das wohl auch ins Forum Mathematik, was? 🙄


  • Mod

    314159265358979 schrieb:

    ...Bei Sockets kann man einfach int nehmen.

    das ist ein trugschluss der mich ein ganzes wochenende debuggen gekostet hat, manchmal ist ein socket kein int oder void*, sondern eine ganze struct. ich wollte einfach nur compile zeit sparen indem ich im header vom wrapper kein <socket.h> include, sondern es bei int belasse -> fail.

    deswegen wuerde ich abraten irgendwelche annahmen zu machen um code zu verbiegen.

    @Waper
    ich glaube nicht, dass es diesen einen perfekten stil gibt, es kommt drauf an was du erreichen willst.

    ich habe opencl/cuda/compute unter einem wrapper, was fuer mich kurz bedeutet, dass es ein include gibt den ich umbiege (deviceCL.hpp, deviceCUDA.hpp, deviceCS.hpp) und damit ist alles auf einer neuen/anderen platform. fuer sowas wie device/Images/memory/kernel hab ich jedesmal eine komplett neue implementierung, die nur die gleichen funktionen bereitstellen. die zeiger auf die einzelnen elemented sind per typedef auf generische namen umgestellt, aber es gibt ausser dem device, keine instanz auf die ein user direkt zugreifen kann und die devices implementieren halt alle dieselben funktionen.

    bei meinem server, der sich plugins laden kann, hab ich hingegen 'interface' klassen (pure abstract), und jedes plugin leitet seine davon ab, die implementierung ist dabei aber komplett versteckt und frei (wird ja aus .so bzw. .dll geladen).

    wenn du also wissen willst, ob das guter still ist, muestest du sagen welche eigenschaften du erreichen willst, und dann kannst du dir vermutlich die antwort selber geben, oder? 🙂

    ach ja, ich glaube dieses unterforum ist nicht so verkehrt, weil wir 'spieleprogrammierer' oeft wrapper schreiben. aber wenn du moechtest, verschieb ich dich woanders hin, mach nur einen ton 😉



  • Waper schrieb:

    Sag mal, ist das so schwer zu verstehen?

    natürlich ist alles viel schwieriger, wenn man sich weigert zu abstrahieren. Tue genau das (indem du eine Sprite Klasse baust), dann brauchst du dich später gar nicht mehr drum kümmern. Oder möchtest du überall in deinem Code unterscheiden müssen, ob du GDI oder SFML als backend hast? Nicht? Na, siehst du.



  • @Waper

    Waper schrieb:

    Sag mal, ist das so schwer zu verstehen?

    Denk ich mir bei dir auch irgendwie 🙄

    Guck dir Pimpl an, wurde dir schon empfohlen.
    Du musst ja keine zusätzliche Indirection machen, wenn du den Compilation-Firewall Effekt nicht willst/brauchst.

    Skizziert:

    // MeinObjektNative.h
    
    class MeinObjektWin { ... }; // oder nochmal in ein eigenes File verpackt, was auch immer
    class MeinObjektOsx { ... }; // ...
    
    #if WINDOWS // nur ein Beispiel, passende präprozessor-konstanten bitte selbst raussuchen
    typedef MeinObjektWin MeinObjektNative;
    #elif OSX
    typedef MeinObjektOsx MeinObjektNative;
    #else
    #error OS not supported
    #endif
    
    // MeinObjekt.h
    #include "MeinObjektNative.h"
    
    class MeinObjekt 
    { 
    public: 
    
         void FunktionDieUeberallGleichImplementiertIst();
         void FunktionDiePlattformspezifischImplementiertIst()
         {
             m_native.FunktionDiePlattformspezifischImplementiertIst();
         }
    
    private:
         MeinObjektNative m_native;
    };
    

    Den ganzen plattformabhängigen Code packst du in MeinObjektWin/MeinObjektOsx etc., d.h. die ganzen plattformabhängigen Hilfsfunktionen sowie Code der diese Hilfsfunktionen aufruft.
    Alles was gleich bleibt kommt nach MeinObjekt.

    Evtl. kann es auch Sinn machen MeinObjektWin/MeinObjektOsx eine gemeinsame Basisklasse zu verpassen, die dann wieder plattformunabhängigen Code enthält.

    Ein Pattern was du dir diesbezüglich angucken könntest wäre das Template-Method-Pattern. Und weil's dazupasst gleich noch das CRTP.

    Manchmal wird auch kaum was oder gar nix übrig bleiben was man in MeinObjekt implementieren könnte, in dem Fall kann man die Klasse genau so gut weglassen, und nur mit den typedefs arbeiten.

    Was jetzt noch bleibt, ist die Schwierigkeit den Code so aufzubauen dass eine Trennung überhaupt sinnvoll möglich ist.
    Dafür gibt's leider keine Silver-Bullet. Ich kann dir nur den Tip geben: code es erstmal für 2 oder 3 Plattformen mit #ifdef, und überführe diese Lösung dann per Refactoring in eine Lösung mit sauberer Trennung der OS-abhängigen Teile.

    Ich möchte daher lieber etwas haben, das den kompilierten Code nicht beeinflusst.

    "Ich will was besseres, aber es darf nicht anders sein" 🙄



  • hustbaer schrieb:

    Ich möchte daher lieber etwas haben, das den kompilierten Code nicht beeinflusst.

    "Ich will was besseres, aber es darf nicht anders sein" 🙄

    Na ja, diese Forderung ist doch durchaus vertretbar. Allerdings wird sie ja auch erfüllt mit eigenen typedefs.



  • Waper schrieb:

    class MeinObjekt  // ein Objekt ist keine Klasse
    {
    public:
    
        void FunktionDieUeberallGleichImplementiertIst();
        void FunktionDiePlattformspezifischImplementiertIst();
    
    private:
    
    #include "MeinObjektPlattformspezifisch.h"  // furchtbar, hast du schon mal ein Codereview auf sowas machen lassen?
    };
    

    sowas hier

    move Win\MeinObjektPlattformspezifisch.h Projekt
    compile Projekt\*.cpp
    move Projekt\MeinObjektPlattformspezifisch.h Win
    

    nennt man dateiorientierte Programmierung, du willst aber doch wohl OOP?

    Schon mal was von Vererbung gehört?
    Bei richtigen OOP-Sprachen kann man gar nicht anders, als Subklassen anzulegen, also z.B.

    class Abstrakt {
    public:
      int ueberallgleich(void) { allgemeines Zeugs }
      virtual int ueberallanders(void) = 0;
    };
    
    #if defined (WIN)
    class Konkret : public Abstrakt {
    public:
      int ueberallanders(void) { DirectX Zeugs }
    };
    
    #elif defined (OPENGLES)
    class Konkret : public Abstrakt {
    public:
      int ueberallanders(void) { OpenGL Zeugs }
    };
    
    #else
    #error muss noch
    #endif
    
    int main() {
      // hier dann natürlich immer nur die konkrete Klasse instanziieren
    }
    


  • @Wutz
    Ich vermute dass er keine virtual calls will. Weil das ja wieder Overhead erzeugt den man sonst nicht hätte.

    Daher vielleicht eher sowas

    // Variante 1
    
    template <class NativeFoo>
    class FooImpl : public NativeFoo
    {
    public:
      int ueberallgleich() { allgemeines Zeugs } // ueberallgleich kann natürlich Funktionen verwenden die in NativeFoo implementiert sind
      // int ueberallanders() erben wir einfach von NativeFoo
    };
    
    // ...
    
    #if defined (WIN)
    typedef FooImpl<WinFoo> Foo;
    #elif defined (LINUX) || defined (BLAH)
    typedef FooImpl<NixFoo> Foo;
    #else
    #error hmpf
    #endif
    
    // Variante 2
    
    class FooImplBase
    {
    public:
        int ueberallgleich_1() { allgemeines Zeugs }
    };
    
    #if defined (WIN)
    
    class WinFooImpl : public FooImplBase
    {
    public:
        int ueberallanders() { DX Zeugs } // kann Funktionen aus FooImplBase verwenden
    private:
        DX Zeugs
    };
    typedef WinFooImpl NativeFooImpl;
    
    #elif defined (LINUX) || defined (BLAH)
    
    class NixFooImpl : public FooImplBase
    {
    public:
        int ueberallanders() { OGL Zeugs } // kann Funktionen aus FooImplBase verwenden
    private:
        OGL Zeugs
    };
    typedef NixFooImpl NativeFooImpl;
    
    #else
    
    #error muss noch
    
    #endif
    
    class Foo
    {
    public:
      int ueberallgleich_2() { allgemeines Zeugs }
      int ueberallanders() { return m_native.ueberallanders(); }
    private:
      NativeFooImpl m_native;
    };
    


  • @Waper
    Du könntest dir auch mal in ein paar realen Open-Source Projekten ansehen wie das dort gemacht wird.
    Beispielsweise XBMC oder auch eine der grösseren Open-Source Game- bzw. 3D-Engines.

    Das ist dann zwar auch wiedr Grafik-Zeugs bzw. Spiele-Zeugs, aber wie rapso schon sagte: in Spiele-Zeugs-Code findet man sowas einfach relativ oft, und die Spiele-Jungs haben daher mit sowas auch relativ viel Erfahrung.

    Ahja, fast vergessen: einige Boost Libraries.
    Manche bestehen quasi nur aus Klassen/Funktionen zum "verstecken" von plattformabhängigem Code (z.B. Boost.Filesystem, Boost.System), und andere "verstecken" plattformabhängigen Code, bauen dann aber selbst weitere Funktionalität darauf auf (z.B. Boost.Asio, Boost.Iostreams).

    Das wäre dann was ausserhalb des Spiele- und 3D-Bereichs.



  • Aha, du machst Designentscheidungen abhängig von vermeintlichen oder tatsächlichen Performanzabhängigkeiten im Promillebereich, da solltest du dann konsequenterweise gleich in C programmieren, da hast du nämlich gar keinen C++ Overhead.



  • Einfach Windows aus der Liste der zu unterstützenden Systemen rausstreichen und die meisten Probleme lösen sich wie von selbst. Posix ftw.



  • 314159265358979 schrieb:

    Einfach Windows aus der Liste der zu unterstützenden Systemen rausstreichen und die meisten Probleme lösen sich wie von selbst. Posix ftw.

    Einfach die Kunden rausstreichen und die Probleme lösen sich wie von selbst.



  • Wutz schrieb:

    Aha, du machst Designentscheidungen abhängig von vermeintlichen oder tatsächlichen Performanzabhängigkeiten im Promillebereich, da solltest du dann konsequenterweise gleich in C programmieren, da hast du nämlich gar keinen C++ Overhead.

    Mache ich nicht, ich versuche lediglich die Frage des OP zu beantworten. Und dich darauf hinzuweisen dass du ihm einen Vorschlag gemacht hast, den er nicht haben will - worauf er auch schon hingewiesen hat.

    Aber mal der Reihe nach.

    1. virtual calls sind eine kaum überwindbare Inlining-Barriere. Die "Performanzabhängigkeiten im Promillebereich" können schnell zu Unterschieden im zweistelligen Prozentbereich werden.

    2. Der OP hat beim Thema Pimpl schon geschrieben dass er das nicht will, weil es zu anderem Maschinencode führen wird als eine naive #ifdef-Orgie. Da virtual calls ganz sicher teurer sind als "einfaches" Pimpl, und vor allem auch zu gänzlich anderem Maschinencode führen werden, gehe ich davon aus dass der OP davon nix wissen will. Warum und ob das Sinn macht ist sein Problem, nicht meins.

    3. Wozu bitte virtual calls? Wenn ich weiss dass das Kompilat X nur auf OS-X laufen wird, und der konkrete Typ hinter dem Zeiger daher in Kompilat X immer der selbe sein wird ... wozu dann bitte virtual? Nur "because we can"? Welches Problem löst es, welchen Vorteil hat man dadurch? Keines und keinen. Also kann ich die konkrete Klasse gleich direkt als Member reinpacken. Ohne Umweg über Zeiger und ohne unnötige virtual Calls. Inlining funktioniert, es kommt Code raus der vermutlich fast identisch mit dem unaufgeräumten #ifdef-Orgien Code ist, alle sind glücklich. Und der Code ist auch nicht mehr oder weniger "OOP" als mit virtual.

    4. Dein Beispiel ist einfach nur Quatsch. virtual + genau eine Implementierung die dann per #ifdef ausgewählt wird. Sorry, aber ... WTF? Irgendwo "OOP" hinschreiben macht aus Quatsch weder objektorientierten Code noch guten Code.


Anmelden zum Antworten