Funktionstemplate überladen



  • Du könntest natürlich sowas machen wie:

    	inline void LoopRangeA(void (*f)(MyStruct*)) {
            for (int i = 0; i < aRange; i++) {
                f(&this->valArr[i]);
            }
        }
    
    	inline void LoopRangeA(void (*f)(MyStruct*, int)) {
            for (int i = 0; i < aRange; i++) {
                f(&this->valArr[i], i);
            }
        }
    

    Aber irgendwie würde ich eher auch mal zum gesamten Design nachfragen. Zum Glück hat sich auch dein Hinweis, dass das Array unterschiedliche Typen beinhaltet, nicht bestätigt (es sind alles MyStructs - ich hatte schon irgendwelche reinterpret_casts befürchtet). Aber generell gesagt kann ich mir kaum vorstellen, dass 20 verschiedene Loop-Funktionen irgendwie sinnvoll sein können. Das klingt irgendwie sehr merkwürdig.

    (auch dass du in dem Lambda int& haben willst, halte ich für fragwürdig und habe oben in der Signatur nur int benutzt)

    Edit: du brauchst nicht unbedingt std::vector, um iterator-Style zu arbeiten. Das geht auch mit Arrays.



  • @CJens

    diesem Grund möchte ich hier auf std::vector verzichten.

    Ich habe nichts von std::vector gesagt. Ein Iterator für das Array ist einfach MyStruct*.



  • @wob Nein, die Typen sind nicht unterschiedlich. Am Ende habe ich ein Array mit Punkten. Die Strukturen sind natürlich alle gleich, aber einige Punkte liegen am Rand des Gebietes, andere im Gebiet selbst.
    Am Ende gibt es noch Flächen. Einige Flächen liegen komplett innerhalb des Gebietes, andere Teilen eine Kante mit dem Rand, andere nur einen Punkt. Alle Punkte sind natürlich vom gleichen Typ, aber für mich sind sie unterschiedlich.

    Am Ende möchte ich die Möglichkeit haben, nur die Flächen, die eine Kante mit dem Rand teilen, nur die Flächen, welche einen Knoten mit dem Rand teilen und nur die Flächen, welche komplett innerhalb des Gebietes liegen, loopen können.

    Die klasse beinhaltet die komplette Anzahl von Flächen, die Anzahl der Flächen, die nur einen Knoten mit dem Rand teilen und die Anzahl der Flächen, die eine oder mehrere Kanten mit dem Rand teilen. Die Anzahl der internen Flächen ergibt sich dann aus Gesamtanzahl_Flächen - (Flächen_mit_Knoten_am_Rand + Flächen_mit_kante_am_Rand). Wegen dieser Rechnerei habe ich auch die Templates geschrieben.

    Danke, ich werde mal Deinen Vorschlag prüfen.



  • @manni66 Achso, ja - das stimmt. Aber die Anzahl der Elemente muss dann berechnet werden.

    Wenn Du Dir meine Antwort an "wob" ansiehst erkennst Du, dass der Ausdruck der for-Schleife recht lang werden könnte. Ich wollte das verhindern. Es ist kein Problem, dass ich nicht anders lösen kann, aber es geht mir eben um das Design. Der User soll nicht die Möglichkeit haben, sich da zu verrechnen.

    Bis jetzt habe ich es genau so gemacht, dass ein Klassenmember mir den Zeiger auf das erste, jeweilige Element zurückgibt und die Anzahl der Elemente. Klar geht das... aber ich möchte es auch gerne anders realisieren und auch dazu lernen 🙂

    Ps.: Das std::to_string auf ein Integer ist natürlich nicht nötig, aber ist es so falsch?



  • @CJens

    Ps.: Das std::to_string auf ein Integer ist natürlich nicht nötig, aber ist es so falsch?

    Wenn man das liest denkt man, dort geschieht etwas das man nicht verstanden hat. In dem Sinne: ja, es ist falsch.



  • @CJens

    dass der Ausdruck der for-Schleife recht lang werden könnte.

    Den Einwand verstehe ich nicht. Ein Iteratorpaar bestimmt Anfang und Ende eines Bereichs. Wie der errechnet wird interessiert beim Iterieren nicht.



  • @manni66 Ich lerne gerne dazu - ich habe in letzter Zeit viel in .Net programmiert und da muss man Integer mit .ToString() in Console.WriteLine ausgeben.



  • @manni66 Also, um mein "richtigen" Beispiel hierauf zu übertragen:

    void LoopTypeA(FUNCTION f){
        MyStruct* pStrArr = this->strArr;
        if(pStrArr == nullptr){throw std::exception("Error: Struct array is not referenced!");}
    
        for(int i = 0; i<this->nRangeA;i++){f(pStrArr++);}
    }
    
    void LoopTypeB(FUNCTION f){
        MyStruct* pStrArr = this->strArr[this->nRangeA];
        if(pStrArr == nullptr){throw std::exception("Error: Struct array is not referenced!");}
    
        for(int i = 0; i<this->nRangeB;i++){f(pStrArr++);}
    }
    
    void LoopTypeC(FUNCTION f){
        MyStruct* pStrArr = this->strArr[this->nRangeA + this->nRangeB];
        if(pStrArr == nullptr){throw std::exception("Error: Struct array is not referenced!");}
    
        for(int i = 0; i<20 - (this->nRangeA + this->nRangeB);i++){f(pStrArr++);}
    }
    

    Da ist es doch deutlich lesbarer, wenn man die Iteration in eine Function packt, finde ich...

    Ich muss dazu sagen, dass es sich um mehrere millionen Strukturen handeln kann und meist auch handelt. Aus Gründen der Performance möchte ich daher gerne mit Zeigerarithmetik arbeiten.



  • @CJens

    auto rangeC()
    {
      MyStruct* begin = this->strArr[this->nRangeA + this->nRangeB];
      MyStruct*  end = begin + 20 - (this->nRangeA + this->nRangeB);
      return std::pair{begin, end};
    }
    


  • @wob Also, es handelt sich um eine Klasse "Mesh". Ein Netz besteht aus Knoten, die können intern (im Gebiet) oder extern (am Rand) sein.
    Kanten stellen die Verbindung aus zwei Knoten dar. Diese können komplett im Gebiet liegen. Sie können auch einen internen und einen externen Knoten haben und sie können komplett am Rand liegen.
    Flächen bestehen aus mehreren Knoten und können ebenfalls intern, teilweise intern und komplett extern sein.
    Volumenzellen bestehen aus mehreren Flächen und können ebenfalls komplett intern sein. Es kann auch sein, dass nur einer ihrer Knoten am Rand liegt oder eine / mehrere Flächen.

    Es gibt so gesehen folgende Loop Funktionen

    void LoopInternalNodes()
    void LoopExternalNodes()
    void LoopAllNodes()

    void LoopInternalEdges()
    void LoopNodalBoundaryEdges()
    void LoopExternalEdges()
    void LoopAllEdges()

    void LoopInternalFaces()
    void LoopNodalBoundaryFaces()
    void LoopExternalFaces()
    void LoopAllFaces()

    void LoopInternalCells()
    void LoopNodalBoundaryCells()
    void LoopExternalCells()
    void LoopAllCells()

    Ok - sind "nur" 15 LoopFunktionen.
    Bin noch am Überlegen, ob ich eine Enumeration erstelle und diese als Argument übergebe um die Anzahl der Loop Funktoinen zu reduzieren. Oder ich packe diese in eine statische Klasse als Member der Klasse.

    Im Array liegen erstmal alle internen Elemente. Dann kommen die Elemente, welche nur einen teil der Knoten auf dem Rand haben. Am Ende kommen die "externen" Elemente. Meine Routine, welche das Gitter liest, ordnet das schon so korrekt an.



  • @manni66 Hey, danke - das gefällt mir.

    Hab es jetzt wie folgt gemacht:

    	template<typename T>
    	class Range {
    	private:
    		T* arrStart = nullptr;
    		T* arrEnd = nullptr;
    
    	public:
    		Range(T* IterStart, T* IterEnd) { this->arrStart = IterStart; this->arrEnd = IterEnd; }
    
    		T* begin() {return arrStart;}
    		T* end() { return arrEnd; }
    	};
    

    In der Klasse gibt es denn eine Funktion:

    Range<MyStruct> RangeA() { return Range<MyStruct*>(this->valArr, &this->valArr[this->aRange]); }
    

    Und jetzt kann ich das rangebased-for nutzen.

    Gibt es für die Range schon eine Klasse in der StandardLib?



  • @CJens sagte in Funktionstemplate überladen:

    Gibt es für die Range schon eine Klasse in der StandardLib?

    Soweit ich weiss wird wohl erst ab C++20 mit std::span eine solche Range wie deine in der Standardbibliothek verfügbar sein. Bisher gibt es nur std::string_view das auf einer ähnlichen Idee basiert, aber eher für Zeichen-Typen gedacht ist und ein String-Interface exponiert.

    Prinzipiell halte ich es auch für eine gute Idee Operationen auf einem Mesh in Form von Ranges umzusetzen. Die Standardbibliothek wird mit C++20 und der Ranges-Bibliothek eine Menge nützliche Algorithmen enthalten um auf auf solchen Ranges wie deinem Range<T> zu arbeiten. Damit lassen sich Operationen auf Mengen von Elementen auch recht elegant formulieren:

    auto internal_node = [](const Node& node) { 
        // Gibt true zurück, wenn node interner Knoten,
        // sonst false.
    };
    auto normalize = [](Node node) { 
        node.position = normalize_vector(node.position);
        return node;
    };
    
    for (auto node : mesh.nodes() | view::filter(internal_node) | view::transform(normalize))
    {
        // Mach irgendwas mit internem Knoten 'node',
        // der hier eine Kopie des Knotens im Mesh ist und
       /// einen normalisiertem Positionsvektor hat.
    }
    

    ("Pipe-Syntax" mit "|" aus der Ranges-Bibliothek von C++20)

    Die Richtung ist also schonmal ganz gut, denke ich 😉
    Auch vor C++20 sind Ranges auch ein guter Ansatz, um bestimmte Probleme anzugehen, allein schon weil man sie bequem in einem Range Based For verwenden kann.

    Edit:
    Vielleicht lassen sich deine o.g. Loop-Funktionen auch reduzieren, indem du entsprechende Filter-Funktionen in der Mesh-Klasse zur Verfügung stellst. Es könnte dann für jeden Typ von geometrischen Elementen nur eine Funktion geben, die eine Range über alle diese Elemente zurückgibt (also quasi nur LoopAll*()):

    mesh.nodes();
    mesh.edges();
    mesh.faces();
    mesh.cells();
    

    Die speziellen Elemente kann man sich dann mithilfe eines Filters herausfischen, der eine gefilterte Range zurückgibt, die nur die Elemente enthält, bei denen die Filter-Prädikat true ist. Das muss nicht wie oben bei dem Beispiel für der C++20-Ranges mit dem |-Operator passieren, du kannst auch einfach eine FilterRange-Klasse schreiben, die eine Eingabe-Range und eine (Filter-)FUNCTION entgegennimmt und dann mit den von begin()/end() zurückgegebenen Iteratoren nur über diejenigen Elemente iteriert, für welche die Filter-Funktion true zurückgibt. Hier nur ein grober Ansatz:

    template<typename InputRange, typename FilterFunction>
    class FilterRange
    {
        public:
            FilterRange(InputRange in, FilterFunction filter);
            ...
    }
    template<typename InputRange, typename FilterFunction>
    auto filter(InputRange in, FilterFunction filter)
    {
        return FilterRange{ in, filter };
    }
    ...
    for (auto node : filter(mesh.nodes(), Mesh::internal_node_filter))
    {
       ...
    }
    

    Bezüglich der Iteratoren könnte man das hier z.B. mit einer eigenen Iterator-Klasse FilterRangeIterator umsetzen, in der der operator++() z.B. das nächste Element in der InputRange sucht, für das filter(node) = true ist, und dieses Element dann zurückgibt.

    Das ist natürlich nur eine lineare Suche über alle Knoten und lässt sich auch optimieren z.B. mit speziellen Iteratoren, die genau wissen, wie sie sich z.B. in einem Mesh gezielt von einem internen/externen Knoten zum nächsten "hangeln" können. Dafür denke ich bräuchte man dann aber wieder extra Funktionen, die entsprechende Ranges zurückgeben, deren Iteratoren dieses "Spezialwissen" mitbringen. Z.B. sowas wie mesh.internal_nodes(). Das nur als Anregung, dass sich so ein range-basierter Ansatz durchaus optimieren lässt.

    Das ist zwar nicht weniger Aufwand zu programmieren, als die ganzen Loop*()-Funktionen, es wäre so in meinen Augen allerdings eleganter und flexibler, da es z.B. auch benutzerdefinierte Filter erlaubt, für die es keine vorgegebene Funktion in der Mesh-Klasse gibt.



  • @Finnegan Wow - ehrlich, das sieht schon sehr professionell aus. Aber mit ist das auch performat?
    Vergiss bitte nicht - ich muss hier mehrfach an die 10 Mio. Knoten in einer inneren Schleife durchlaufen. Deshalb habe ich eine sehr aufwändige DLL geschrieben, welche mir alle Elemente des Gitters bereits sortiert anlegt.

    Na, dann werde ich nicht auf C++20 warten - ist ja auch nicht sooo schwer, das selbst zu programmieren 😉

    Vielen Dank,
    dem Beispiel werde ich mir trotzdem sehr viel abschauen 😉

    Jan



  • @CJens sagte in Funktionstemplate überladen:

    @Finnegan Wow - ehrlich, das sieht schon sehr professionell aus.

    Soll nur eine Anregung sein, wie das mal aussehen könnte, wenn man das mit den Ranges voll durchzieht. Schreibe selbst viel Bibliotheks-Code und wollte dich nicht überrumpeln 😉

    Ist natürlich so erstmal etwas aufwändiger umzusetzen, hat aber den Vorteil, dass das, was am Ende rauskommt recht flexibel und "leicht zu bedienen" ist (für den "Endbenutzer").

    Aber mit ist das auch performat?
    Vergiss bitte nicht - ich muss hier mehrfach an die 10 Mio. Knoten in einer inneren Schleife durchlaufen. Deshalb habe ich eine sehr aufwändige DLL geschrieben, welche mir alle Elemente des Gitters bereits sortiert anlegt.

    Wenn das gut umgesetzt ist, dann kann das genau so schnell wie "manuell" geschriebene Schleifen sein. Solche Range- und Iterator-Klassen sehen erstmal nach mehr Arbeit für den Rechner aus, wegen der zusätzlichen Funktionsaufrufe und temporären Klasseninstanzen, die da erzeugt werden. Diese zerfallen aber oft unter Compiler-Optimierungen zu nahezu dem selben Maschinencode, der auch bei manuellen Schleifen generiert worden wäre, sofern sie leichtgewichtig implementiert werden (Inlining, RVO, etc.).

    Leichtgewichtig heisst hier, dass sie vor allem kaum Daten-Member haben. Im Prinzip sollten das alles nur Klassen sein, die z.B. nur einen Node-Pointer halten. Das "Spezialwissen", wie die Datenstruktur zu iterieren ist, steckt in dem Code, der zum nächsten Element "iteriert", z.B. dem operator++(). Ein optimierender Compiler erzeugt aus sowas dann durchaus gerne eine simple Maschinencode-Schleife, die den operator++()-Code direkt im Schleifenkörper enthält und den Pointer in einem Register hochzählt. Natürlich alles nicht garantiert, man muss sich schon auf die Optimierungs-Magie des Compilers verlassen - diese "Magie" ist aber meist recht gut, es lohnt sich mal anzuschauen, was Compiler so aus manchem umständlich aussehenden Code machen (Iteratoren, Proxy-Klassen und ähnliches).

    Sicher werden einige Optimierungen nötig sein, wenn du wert auf Performance legst - wie ich erwähnt habe, können das z.B. Iteratoren mit "Spezialwissen" sein, die genau wissen, wie sie die Knoten für eine bestimmte Fragestellung möglichst effizient zu durchlaufen haben. Z.B. könnte ein Iterator für äußere Knoten immer am Rand des Mesh entlang laufen anstatt stumpf jeden Knoten zu prüfen, ob dieser ein äußerer Knoten ist.

    Na, dann werde ich nicht auf C++20 warten - ist ja auch nicht sooo schwer, das selbst zu programmieren 😉

    Mit ein paar guten Ideen kann man sich schon jetzt anfreunden - auch wenn es noch keine Ranges-Bibliothek gibt, lassen sich einige Probleme elegant lösen, wenn man sich da schonmal was abguckt.

    Vielen Dank,
    dem Beispiel werde ich mir trotzdem sehr viel abschauen 😉

    Dafür ist es gedacht. Es ist ohnehin kein vollständiges Beispiel, nur ein grober Ansatz, wie das aussehen könnte. Der eigentlich interessante Teil ist natürlich die konkrete Implementierung der Range- und Iterator-Klassen. Das wäre aber etwas viel und mir auch zu mühselig geworden. Abgesehen davon, dass ich gar nicht weiss wie deine Mesh-Datenstruktur überhaupt von innen aussieht 🙂

    Gruss,
    Finnegan



  • Wenn du jetzt schon mit Ranges arbeiten willst, schau dir gerne mal an: https://github.com/ericniebler/range-v3


Anmelden zum Antworten