virtual function templates



  • Das schreit nach Type-Erasure / Polymorphie / Callbacks.

    Vielleicht so:

    template<class T>
    class abstract_range
    {
    protected:
      abstract_range() {}
      ~abstract_range() {}
    public:
      typedef typename std::remove_cv<T>::type value_type;
      typedef T& reference;
    
      virtual bool empty() const = 0;
      virtual reference front() const = 0;
      virtual void pop_front() = 0;
    };
    
    void print(abstract_range<const A> && rng)
    {
      while (!rng.empty()) {
        cout << rng.front();
        rng.pop_front();
      }
    }
    
    class dings : public abstract_range<const A>
    {
      …
    };
    
    int main() {
      vector<unique_ptr<A>> bums;
      …
      print(dings(bums));
    }
    

    Das, was ich hier "dings" genannt habe, könnte man auch noch als viel generischeren Iterator-Wrapper aufboren, der mit einem Optionalen Funktionsobjekt auch noch die Dereferenzierung eines Zeigers erlaubt:

    template< … >
    class … : public abstract_range< … >
    {
      …
      bool empty() const {
        return this->begin_iter == this->end_iter;
      }
    
      reference front() const {
        return this->funcobj(this->begin_iter);
      }
    
      void pop_front() {
        ++(this->begin_iter);
      }
      …
    };
    

    Die Sache mit der Laufzeitpolimorphie ist wie eine Pest. Hat man damit im Design einmal mit angefangen, wird man sie nicht so schnell wieder los. Ggf brauchst du gar keine virtuelle Methode namens foo. Falls doch, kann man sich eventuell eine bessere Schnittstelle überlegen, die nicht für jedes Element eine virtuelle Funktion aufrufen muss, sondern Daten vielleicht in größere Häppchen liefert.



  • dot schrieb:

    Erklär mal genauer was A und foo eigentlich sind und wieso foo virtual sein muss.

    Offen gesagt hatte ich das Problem schon öfter, wobei foo nicht immer virtual sein musste, weshalb ich teilweise auf Templates ausweichen konnte. Schön fand ich das aber noch nie, wenn der Typ von *iterator schon feststeht.

    Beispiele wären z.B. ein Server der auf Clienten arbeiten soll ("handleClients(..)") (nicht virtual), oder ein Spielobjekt, das eine "tick()" Funktion hat. (virtual)

    So etwas wäre schön:

    void foo(Iterator<A*> begin, Iterator<A*> end)
    { // "Gib mir Zeiger, deren arithmetisches Verhalten egal ist, die aber auf einen A* zeigen."
    }
    

    Der unique_ptr würde natürlich bleiben. Aber damit könnte ich noch leben denke ich.
    Nur stehe ich gerade etwas auf dem Schlauch. Irgendwie schaffe ich es nicht mal, den Typ des Iterators in einer Klasse zu verstecken.

    Wenn man mal bei dem Spielobjekt mit dem tick() bleibt, sollte ein weiterer virtueller Aufruf durch den Iterator natürlich vermieden werden.
    (Edit: Wobei. Kommt darauf an. Wann hat man schon mal > 10k Objekte im Spiel? Da sind vielleicht sogar virtuelle Iteratoren gar nicht so schlimm..)



  • cooky451 schrieb:

    So etwas wäre schön:

    void foo(Iterator<A*> begin, Iterator<A*> end)
    

    Nein, wärs nicht. Dafür gibt es boost::indirect_operator , dann kannst du bei Iterator<A> bleiben.

    cooky451 schrieb:

    Beispiele wären z.B. ein Server der auf Clienten arbeiten soll ("handleClients(..)") (nicht virtual), oder ein Spielobjekt, das eine "tick()" Funktion hat. (virtual)

    Versteh ich nicht.
    Meistens kannst du ein handleClients (nicht virtual) machen, das ein handleSingleClient (virtual) aufruft. Also das du das Iterieren in ein Non-Virtual Interface verschiebst.

    Jedenfalls ging das bei mir und ich hatte einen abstract_range nie nötig.



  • indreckt schrieb:

    Jedenfalls ging das bei mir und ich hatte einen abstract_range nie nötig.

    Das wird aber schwer, wenn ein Clienten interagieren sollen. 😉



  • cooky451 schrieb:

    … jetzt sehe ich auch noch krümelkackers Beitrag. (Das … kommt wohl in Mode…)
    Von welchem Typ sind denn bei dir begin_iter und end_iter?

    template <class Iter>
    struct IterRange : abstract_range<typename std::iterator_traits<Iter>::value_type>
    {
      Iter begin_iter;
      Iter end_iter;
    
      IterRange(Iter first, Iter last);
      …
    };
    
    template <class Iter>
    IterRange<Iter> make_range(Iter first, Iter last)
    { return IterRange(first, last); }
    
    template <class Container>
    IterRange<typename Container::iterator> make_range(Container& cont)
    { return IterRange(cont.begin(), cont.end()); }
    
    template <class Container>
    IterRange<typename Container::const_iterator> make_range(Container const& cont)
    { return IterRange(cont.begin(), cont.end()); }
    
    //usw...
    

    Anderer Ansatz, um das virtual in der Klasse zu lassen und nicht irgendwelche Iteratoren abstrahieren zu müssen: Template Method Pattern.

    Allgemeine, aufgebohrte Overkill-Version:

    struct A
    {
      template <class Iter>
      void foo(Iter first, Iter last)
      {
        static_assert(std::is_same<A, typename std::iterator_traits<Iter>::value_type>::value, "foo nur mit Iteratoren auf A aufrufen!")
    
        begin_foo();
        if (first != last)
          do_foo(*first++);
        for(; first != last; ++first)
        {
          between_foo();
          do_foo(*first);
        }
        end_foo();
      }
    
      virtual void begin_foo();
      virtual void do_foo(A&);
      virtual void between_foo();
      virtual void end_foo();
    };
    


  • cooky451 schrieb:

    So etwas wäre schön:

    void foo(Iterator<A*> begin, Iterator<A*> end)
    { // "Gib mir Zeiger, deren arithmetisches Verhalten egal ist, die aber auf einen A* zeigen."
    }
    

    Bekommst Du:
    vector<unique_ptr<A>>::iterator --> boost::indirect_iterator<…> --> any_iterator<A>

    wobei der any_iterator ähnlich wie boost/std::function auf den Freispeicher ausweicht, was eigentlich nur dann nötig ist, wenn man diese Iteratoren per Wert übergeben will und wenn sie kopierbar sein müssen. Das habe ich mir bei abstract_range vorhin extra gespart. Auch blöd bei diesem any_iterator-Ansatz ist, dass man sie vergleichen können sollte. In der entsprechenden operator== und operator!= Funktion wird erstmal geprüft, ob die versteckten Typen überhaupt zusammen passen. Das ist eben der Haken daran, wenn man zwei Objekte braucht, um eine Sequenz darzustellen.



  • Wenn du einfach nur die tick() Methode all deiner Spielobjekte aufrufen willst, dann mach das doch einfach? Wieso musst du da irgendwo eine abstrakte Collection dieser Objekte an eine virtuelle Methode übergeben?



  • pumuckl schrieb:

    krümelkackers Code Erklärung

    Ja, das habe ich dann auch gemerkt. ^^

    pumuckl schrieb:

    Anderer Ansatz, um das virtual in der Klasse zu lassen und nicht irgendwelche Iteratoren abstrahieren zu müssen: Template Method Pattern.

    Das Problem ist halt, dass die Objekte / Clienten untereinander agieren müssen, soweit ich das hier sehe funktioniert das so leider nicht.

    Hm.. ich glaube ich schreibe erst mal eine std::list<std::unique_ptr<A>>::iterator Version, und teste irgendwann später die Version mit den virtuellen Iteratoren. Dann habe ich auch gleich einen direkten Geschwindigkeitsvergleich.

    any_iterator habe ich auch schon gefunden, aber das ganze scheint mir etwas langsam. Eigentlich ähnlich der Lösung von krümelkacker?

    @dot
    Wie sollen sich denn Objekte in einer Welt zurechtfinden, in der sie nicht mit den anderen Objekten kommunizieren können? Angenommen bei einem Strategiespiel hat man eine Einheit, die feindliche Objekte im Radius von 5 Metern automatisch angreift. Wie soll diese Einheit denn nach feindlichen Objekten suchen?



  • Objekte werden normalerweise in einer Welt verwaltet. Eine Einheit fragt einfach die Welt "gib mir alle Objekte im Bereich ...". Danach kann ich entscheiden, welches Objekt angegriffen wird. Wenn man will kann man das gern in Sensoren, Aktuatoren, Trigger, etc. aufteilen.



  • cooky451 schrieb:

    Angenommen bei einem Strategiespiel hat man eine Einheit, die feindliche Objekte im Radius von 5 Metern automatisch angreift. Wie soll diese Einheit denn nach feindlichen Objekten suchen?

    Gar nicht. Wieso sollte die Einheit sämtliche Objekte der Spielwelt kennen und diese dann auch noch durchsuchen? Die Einheit kann niemals genug Information haben um das auch nur annähernd effizient durchzuführen.
    Aber irgendjemand kennt alle Objekte und hat die evtl. auch schon in einer Datenstruktur organisiert, mit der solche Abfragen effizient durchgeüfhrt werden können. Die bessere Lösung wäre also, dass deine Einheit einfach diesen jemand nach den 5 nähesten gengerischen Einheiten fragt 😉



  • Gib mal eine grobe Beschreibung deiner Gegebenheiten ab, aber etwas weniger abstrakt als A::foo 😉

    Ich vermute jetzt mal, dass du folgendes hast:

    a) Einen Haufen Spielobjekte
    b) Einen oder mehrere Container, die diese Spieolbjekte beinhalten

    A::foo() ist dann eine Methode dieser Spieolbjekte, mit der du WAS machen möchtest?



  • Da das mit den Iteratoren ja mehr oder weniger durchgekaut ist, können wir ja auch gleich mit Spieledesign weitermachen. 🤡

    Der Plan war eigentlich, quasi das gesamte Geschehen Objektintern zu regeln. Soll heißen, im Mainloop gibt es irgendwo so etwas:

    auto elpased_time = timer.tick();
    for (auto i = begin(objects); i != end(objects); ++i)
      (*i)->tick(elapsed_time, begin(objects), end(objects));
    

    So weit, so gut. Alle Objekte können jetzt in tick() machen was sie wollen. Das Grundgerüst schien mir eigentlich relativ angenehm.

    @dot:
    Das bedeutet, du würdest also für jede Objektinteraktion irgendeine Klasse einbauen, die das ganze regelt? Ich habe irgendwie noch nicht ganz verstanden wie sich ein Objekt zurechtfinden soll, ohne die anderen Objekte zu kennen.



  • cooky451 schrieb:

    @dot:
    Das bedeutet, du würdest also für jede Objektinteraktion irgendeine Klasse einbauen, die das ganze regelt? Ich habe irgendwie noch nicht ganz verstanden wie sich ein Objekt zurechtfinden soll, ohne die anderen Objekte zu kennen.

    Dein Objekt braucht die anderen Objekte nicht kennen. Dein Objekt braucht nur ein Objekt zu kennen, welches ihm sagen kann welche Objekte sich im Umkreis von 5m befinden oder so...



  • cooky451 schrieb:

    Alle Objekte können jetzt in tick() machen was sie wollen.

    Richtig. allerdings MÜSSEN sie so auch alles selbst machen. Denk ans "Single Responsibility Principle". Deine "Panzer" sind dafür zuständig, die anderen abzuschießen. Dazu schauen sie in ihre "Umgebung", wer da ist, und rechnen nicht für alle Objekte der großen weiten Welt mit Zettel und Stift selber aus, wer denn da alles nicht ist.
    Die "Umgebung" sollte ein eigenes Objekt sein, das auf solche Distanzberechnungen spezialisiert ist - schließlich werden nicht nur die Panzer sondern auch andere solche Berechnungen benötigen 😉

    Range operator"" _m(char const* meters); //yes, we can in C++11!
    
    void Panzer::tick()
    {
      auto inRange = map.getObjectsInRadius(*this, 5_m);
      for (auto targetIter = inRange.begin(); targetIter != inRange.end() && shotsRemain(); ++targetIter)
      {
        if (isEnemy(*targetIter))
        {
          shootAt(*targetIter);
        }
      }
    }
    


  • Leute, packt mal die Kanonen wieder ein, ihr verschreckt die Spatzen.

    struct A 
     { 
       virtual void foo(std::list<std::unique_ptr<A>>& as) 
       {} 
     };
    

    Einfache und oft anwendbare Lösung:

    struct A 
    { 
       template <class T>
       void foo(T const& as)
       {
           // ... Vorbereitung ...
    
           BOOST_FOREACH(typename T::reference a, as) // ich hoffe das typename T::reference passt, aber wenn nicht so, dann so ähnlich :D
               do_one_foo(*get(a));
    
           // ... Nacharbeiten ...
       }
    
    private:
       virtual void do_one_foo(A& a) 
       {} 
    };
    

    Tadaaaaa. Und ganz ohne Type-Erasure oder 100 Hilfsklassen 😉



  • pumuckl schrieb:

    Range operator"" _m(char const* meters); //yes, we can in C++11!
    

    Warum char* und nicht long double ?



  • knivil schrieb:

    Warum char* und nicht long double ?

    Touché, natürlich 🙂


Anmelden zum Antworten