Virtuelle Methoden auch ohne 'new'?



  • Bitte steinigt mich nicht für die kommende Frage!
    Da ich es noch nie anders gesehen habe, frage ich einfach zu Sicherheit mal nach: Muss ein Objekt zwangsläufig mit 'new' erstellt werden, damit 'virtual' korrekt Funktioniert? Der folgende Beispielcode funktioniert zumindest einwandfrei:

    class Base {
    public:
    	virtual void test123() = 0;
    };
    
    class DerivedA : public Base {
    public:
    	void test123() {
    		std::cout << "A";
    	}
    };
    
    class DerivedB : public Base {
    public:
    	void test123() {
    		std::cout << "B";
    	}
    };
    
    int main() {
    	DerivedA a;
    	Base* base = &a;
    	base->test123();
    
    	DerivedB b;
    	base = &b;
    	base->test123();
    	return 0;
    }
    

    Eine weitere Frage: Was kostet eigentlich 'virtual', wenn man direkt die Methode der test123 von DerivedA bzw. DerivedB benutzt, anstatt über Base? Also z.B. so:

    DerivedA a;
    	a.test123();
    

  • Mod

    Nein, du brauchst nicht zwangsläufig new. new brauchst du sowieso nie, da du andere Methoden benutzen solltest, um Objekte dynamisch anzulegen. Aber damit virtuelle Funktionen funktionieren, muss ein Objekt nicht einmal dynamisch erzeugt werden, denn virtual und dynamische Speicherverwaltung haben technisch gesehen nichts miteinander zu tun. Sie gehen bloß oft Hand in Hand, da virtuelle Funktionen gerade dafür da sind, wenn man nicht genau weiß, welchen Typ ein Objekt genau hat, was fast immer darauf hindeutet, dass diese Information erst zur Laufzeit (also dynamisch) feststehen wird.

    Zur zweiten Frage: Da steht zur Compilezeit fest, was genau passieren wird, es sollte also kein Code zum dynamischen Dispatch erzeugt werden.



  • Genau die Antwort die ich mir erhofft hatte, danke!

    Primär geht es mir um Microcontroller (Cortex M4, 256KB Ram, 1MB Flash). Ich habe die letzten paar Wochen mit Template-Metaprogrammierung experimentiert um möglichst flexible Module zu schreiben die maximale Performance bieten. Jetzt versuche ich das gleiche System nochmals ohne Templates umzusetzen um dann zu vergleichen, was es wirklich bringt.

    Auch zu Laufzeit ein wenig Flexibilität hat schon so seine Vorteile ...

    Aber ich möchte auch, dass die Module mit möglichst wenig Overhead verwendet werden können. Ist beispielsweise schon zur Compilierzeit bekannt, welcher Uart verwendet werden soll, dann kann man eben die 'Uart0' Klasse direkt benutzen, anstatt die Basisklasse 'Uart'.



  • Du könntest z.B. auch die Basisklasse als Template mitgeben.
    Also quasi

    template <class Base>
    class Uart0Impl : public Base
    {
    public:
        ...
        void Write(char const* bytes, size_t byteCount)
        {
            ...
        }
        ...
    };
    
    class AbstractUart
    {
    public:
        ...
        virtual void Write(char const* bytes, size_t byteCount) = 0;
        ...
    };
    
    class Empty {};
    
    typedef Uart0Impl<AbstractUart> PolymorphicUart0;
    typedef Uart0Impl<Empty> NonPolymorphicUart0;
    

    So lange du NonPolymorphicUart0 verwendest, kannst du auf jeden Fall sicher sein dass du bei den Aufrufen keinen virtual-call Overhead haben wirst, weil NonPolymorphicUart0 keine virtuellen Funktionen hat.
    Ansonsten kommt es darauf an wie gut der Compiler optimieren kann.

    Und natürlich, falls der Code für die Uarts identisch sein sollte, weil es z.B. nur Unterschiede bei den Registeradressen gibt, kannst du die Klasse "klassisch" parametrisieren (z.B. Registeradressen im Ctor mitgeben und als Member abspeichern).
    Das kann evtl. eine Spur langsamer sein als wenn Konstanten verwendet werden, aber der Unterschied sollte sehr gering sein. Und einen Vorteil hätte das auch, nämlich weniger Code-Bloat -- was speziell auf kleinen Embedded Systems eine Rolle spielen kann.



  • Ja, das sieht auch ganz gut aus ...

    Ich glaub 'die' Lösung gibt es einfach nicht. Da die Bauteile für mein Projekt bald da sind, muss ich langsam zu einem Ergebnis kommen. Ich spiele grade mit dem Gedanken, wieder ein paar Schritte zurück zu gehen:

    Ich weiß welche Module ich für mein Projekt benötige:
    1x UART
    1x Ethernet
    1x USB
    1x SPI (SD-Card)
    9x GPO
    8x GPI

    Die Software ist an sich nicht besonders komplex (kein RTOS, aber sehr sehr viele Interrupts mit strengen Echtzeitanforderungen). Und wenn die Software dann in ein paar Wochen fertig (und getestet) ist, ist es sehr unwahrscheinlich, dass noch größere Veränderungen vorgenommen werden.

    Bisher habe ich die Software immer so aufgebaut, dass ich auf der untersten Ebene die Treiber habe. Jedoch anstatt einen Treiber zu schreiben der beispielsweise jeden UART unterstütz, habe ich eben nur einen Treiber der nur den verwendeten UART unterstützt. Erst wenn mehrere UARTs verwendet werden habe ich einen allgemeinen Treiber geschrieben.

    Beispielsweise so für einen UART:

    namespace dev {
    namespace uart {
    
    constexpr UART_Type* k_base = UART0;
    
    bool init(uint32_t baudrate) {
      k_base->...
    }
    
    // ...
    
    } // uart
    } // dev
    

    Der Code (und der Maschinencode) wird dadurch sehr schlank und überschaubar. Und was die Peformance angeht ist das wohl auch optimal (mit statischen template-Klassen natürlich auch vergleichbar, aber deutlich mehr Programmieraufwand). Ein spezialisiertes Modul habe ich auf jeden Fall deutlich schneller geschrieben und getestet als ein allgemeines Modul für alle UARTs.

    Mein Frage ich nun: Ist diese Vorgehensweise ok, oder wird man für sowas eher verpönt?



  • Nicht mehr machen als erstmal nötig ist, ist auf jeden Fall schwer OK.
    Umgekehrt: krampfhaftes Vorausplanen, "das könnten wir irgendwann vielleicht mal brauchen" etc. ist meist ein schwerer Fehler.

    Wichtig beim "nur machen was nötig ist" ist bloss dass man ein sauberes Software-Design hat. Das kann man dann später leicht erweitern.

    ps: Weil du schreibst "einen allgemeinen Treiber"... Wie ähnlich sind sich denn die verschiedenen UARTs? Ich würde nämlich eher annehmen dass man für unterschiedliche UART Typen unterschiedliche Treiber schreibt.



  • Auf dem Chip sind insgesamt 6 UARTS, jedoch mit kleinen Unterschieden wie der FIFO Tiefe, HW-Flow-Control, etc ...

    Was mich am OOP-Design stört ist der damit verbundene Overhead. Ich dachte zuerst: "Hey, ein kleiner this-Zeiger macht es nicht viel langsamer." Leider falsch gedacht. Dadurch das die Member keine wirklichen Konstanten sind ist der Compiler was die Optimierungen angeht deutlich eingeschränkter. Mal ganz davon abgesehen, dass die Interrupts so nicht mehr direkt verarbeitet werden können, da der Zeiger auf das Obejekt irgentwo gespeichert werden muss. Und bei 10000 Interrupts/sec (von nur einem Modul) merkt man das leider irgendwann, dass dem Controller die Puste ausgeht.

    Bei der variante mit spezialisierten Treibern im nicht OOP-Stil hingegen finde ich es schon sehr erstaunlich wie effizient der Compiler optimiert.

    Im Prinzip habe ich 4 verschiedene Designs zur Auswahl:

    1. Modular: 1 UART treiber, alle Funktionen liegen in dem Namespace uart. Welcher UART verwendet wird kann nicht zur Laufzeit ausgewählt werden. Alle Funktionen sind direkt für diesen UART optimiert (es muss nicht zur Laufzeit in irgendwelchen Tabellen gestöbert werden).
    Vorteil: Sehr effizient (schnell und speicherschonend)
    Nachteil: Wenig flexibel

    2. Template-Klassen
    Wenn richtig gemacht, genauso effizient wie 1. Außer die Klassen haben nicht statische Member (z.B. ein FIFO-Speicher).

    3. 1 Klasse die die Parameter als Member hält (welcher UART, welcher HW-FIFO-Tiefe. etc ...). So macht es ja z.B. MBED/Arduino/Linux. Also die Platformen wo mehr Wert auf flexibilität und einfache Nutzung anstatt auf Perfomrance gelegt wurde.
    Vorteil: Sehr flexibel, der Anwender muss z.B. dem Konstruktor nur eine andere Zahl übergeben und es wird ein anderer UART verwendet.
    Nachteil: Sehr viel Overhead.

    4. Jeder UART hat seine eigene Klasse mit spezialisierten Methoden. (Könnte dann auch wie 2. programmiert werden). Der UART erbt von einer Basis-Klasse die ein einheitliches Interface nach außen bereit stellt. Ist zur Compilierzeit bekannt, welcher UART verwendet wird, wird eben direkt die spezialisierte UART Klasse verwendet (praktisch kein Overhead). Ist erst zur Laufzeit bekannt, welcher UART verwendet wird, wird eben die Basisklasse verwendet. (Nur der Overhead durch die virtuellen Methoden). Interessant finde ich jedoch, dass der Code einfach mal um 60KB größer wird, nur weil ein virtueller Destructor verwendet wird ...

    Ich tendiere halt grade eher zu 1. In C Projekten habe ich diese Variante schon öfter gesehen, in C++ aber wenn ich so drüber nachdenke noch nie ...
    Im Endeffekt muss ich ja nur die Variante wählen von der ich meine, dass sie den aktuellen Anforderungen entspricht ...

    Verflucht seist du C++, mit deinen unzähligen Möglichkeiten! 😃

    Ich fürchte ich habe einfach zu lange nur für den PC programmiert und sollte nicht mehr Versuchen alles zwanghaft im OOP-Stil umzusetzen.

    Wie Stroustrup sagte:

    The strength of OOP is that there are many problems that can be usefully expressed using class hierarchies - the main weakness of OOP is that too many people try to force too many problems into a hierarchical mould. Not every program should be object-oriented.

    Vielleicht hat er ja genau sowas gemeint ...


Anmelden zum Antworten