std::function best practice?



  • Hi zusammen

    Was ist der best practice wenn ich aus einem Objekt heraus ein Callback aufrufen welches mir der Nutzer des Objekts bereitstellt?

    1. das Callback im CTR bereitstellen und als Member speichern
    template<class T>
    class UpdateableGroup
    {
    public:
        enum class UpdateAction { Create = 0, Update = 1, Remove = 2 };
        using UpdateCallback = std::function<void( const T &, UpdateAction )>;
    
    public:
        UpdateableGroup( UpdateCallback f_ ) : fCallback( f_ ) {}
    
        void update( std::vector<T> item );
        void update( T );
    
    private:
        std::vector<T> Data;
        UpdateCallback fCallback;
    };
    
    
    1. Das Callback bei jedem Call von "update" mitgeben
    template<class T>
    class UpdateableGroup
    {
    public:
        enum class UpdateAction { Create = 0, Update = 1, Remove = 2 };
        using UpdateCallback = std::function<void( const T &, UpdateAction )>;
    
    public:
        UpdateableGroup() = default;
    	
        void update( std::vector<T> item, UpdateCallback f_ );
        void update( T, UpdateCallback f_ );
    
    private:
        std::vector<T> Data;
    };
    

    Es geht hier im stark reduzierten Anwendungsfall um einen Container, der Daten enthält und bei der Presentation neuer Daten, diese vergleicht und im Fall von Unterschieden ein Update-Callback aufruft. D.h. das Callback wird nicht zwangsläufig bei jedem Call von Update gebraucht, sondern nur wenn die Daten sich verändert haben.

    Der Nachteil von Version 1 ist, dass man hier keinen Default-CTR hat und das Callback-Member bei Copy und Move ebenfalls kopiert und bewegt werden muss, was sich für mich irgendwie falsch anfühlt. Aber hier kann ich mich auch täuschen.

    Der Nachteil von Version 2 ist, dass man das Callback dann im aufrufenden Kontext stets bereithalten muss, oder bei jedem Call std::bind aufruft.

    Was wäre hier das best-practice?



  • Kommt doch ein bisschen darauf an, ob alle update(...) aufrufenden Instanzen die Callback-Funktion kenne müssen/sollen. Also wer ruft update(...) auf?



  • In meinem aktuellen Anwendungsfall stellt der Aufrufer immer selbst die Funktion. Egal ob sie im Constructor übergeben wird, oder im Aufruf von "update". Aber für zukünftige Anwendungsfälle kann ich das natürlich nicht sagen, insofern ist deine Frage nachvollziehbar. Das ist tatsächlich ein Punkt den ich berücksichtigen muss.



  • Was passiert denn eigentlich wenn ich ein std::function-Objekt mit std::move verschiebe?

    std::function<void( int )> x = std::bind( ... foobar ...  );
    auto y = std::move( x );
    

    Was passiert, wenn jetzt x( ... ) gerufen wird?
    Ich nehme an er wirft eine Exception?



  • @It0101 Würde ich auch von ausgehen. Godbolt gibt uns auch recht 😉 : https://godbolt.org/z/fdz8zKev3



  • @Schlangenmensch sagte in std::function best practice?:

    @It0101 Würde ich auch von ausgehen. Godbolt gibt uns auch recht 😉 : https://godbolt.org/z/fdz8zKev3

    Ob man sich darauf verlassen kann? Wenn nicht anders spezifiziert, ist x in einem "valid but unspecified state". std::fuction wirft dagegen, wenn sie "leer" ist. Die Frage ist aber, muss ein move dazu führen, dass das Function-Objekt leer ist? Daher: ich wäre mir nicht so sicher, dass das vom Standard unbedingt gefordert ist. Generell würde ich einfach eine moved-from Variable entweder out of scope gehen lassen oder ihr direkt was anderes zuweisen. Alles andere ist fehleranfällig, selbst wenn es mal irgendwo ein garantiertes Verhalten geben sollte.



  • Hm... jetzt habe ich nochmal etwas mehr dazu gelesen.
    "valid but unspecified state" heißt, man darf alle Operationen ausführen, an die keine Vorbedingungen geknüpft sind. Soweit ich das sehe, gibt es keine Vorbedingungen für den Operator ().
    Aber, was dann gemacht wird, hängt vom Zustand des Objektes ab, der aber "unspecified" ist. Könnte dann auch heißen, das die Funktion, die da mal drinnen gesteckt hat, ausgeführt wird, oder?

    Wie auch immer, ich würde nach einem move auch nicht mehr auf dem Objekt weiterarbeiten, die Beschäftigung mit der Frage ist rein Interessen getrieben.



  • Das würde ja eigentlich bedeuten, dass es eher best-practice wäre, das Callback immer nur beim Aufruf der Update-Methode mitzugeben, um das Kopieren/Bewegen der std::function zu vermeiden. Desweiteren gewinne ich einen Default-Konstruktor. Fühlt sich für mich aktuell besser an.



  • Kommt auch dem Gedanken näher, dass eine Updatefunktion eigentlich nichts von ihrem Aufrufer wissen sollte bzw. es müsste ja dann immer gewährleistet sein, dass die Callback-Funktion während der Lebenszeit von einer UpdateableGroup immer "gültig" ist.
    Und es könnte mit deiner letzten Idee irgendwann mal mehrere verschiedene Aufrufer der Updatefunktion geben, die jeweils eine eigene CallBack übergeben.



  • Du könntest auch die Update-Funktion selbst zu einem Template machen und einen Funktor beliebigen Typs als Parameter nehmen (=nicht zwangsläufig std::function).

    Was das beste ist kommt wirklich drauf an was man typischerweise mit der Klasse anstellen will.

    • Kann es überhaupt Sinn machen bei verschiedenen update Aufrufen verschiedene Funktoren mitzugeben?
    • Wie langlebig sind die UpdateableGroup Objekte typischerweise?
    • Könnte es relevante Anwendungsfälle geben wo der indirect-call Overhead von std::function sich negativ bemerkbar macht?
    • Könnte es Fälle geben wo du ein UpdateableGroup in Modul A erstellen möchtest und dann an Module B übergeben, und Modul B ruft dann update auf? Wenn ja, wer sollte in diesem Fall den Funktor bereitstellen? Modul A oder Modul B?

    etc.

    ps: Ich kann mich nicht erinnern schonmal CTR als Abkürzung von "constructor" gelesen zu haben. Hat auch einige Zeit gedauert bis ich verstanden habe was du damit meinst. Verwende doch bitte stattdessen die Standard-Abkürzung "ctor".



  • @hustbaer
    Insbesondere der letzte Punkt in deiner Aufzählung hat mich bereits zuvor stark beschäftigt. Wer stellt den Functor/das Callback bereit? Das ist die eigentlich essentielle Frage, die dieses Konstrukt kompliziert macht, weshalb ich es mittlerweise verworfen habe.

    Der Hintergrund ist:
    Ich bekomme ein vollständiges Konfigurationsobjekt geliefert, welches gewissermaßen für meine Applikation eine Liste von Datenquellen enthält, also eine Art Verbindungsstruktur.
    Eine Liste von Services ( Service 'A', Service 'B', etc. ). Jeder Service enthält eine Liste von Partitionen von denen jede unterschiedliche Daten enthält. Jede Partition wiederum hat 1 bis N Connections ( Stichwort Failover ), die aber jeweils identische Daten liefern.
    D.h. ich muss im Grunde einen Baum abbilden, der aber dynamisch sein kann, was bedeutet, dass ich jederzeit für jeden Service ein Update bekommen kann, das mich dazu zwingt, die exisiterenden Connections zu trennen und neue aufzubauen. Diese Veränderungen wollte ich einem allgemeinen Container abbilden. Die Services wären dann in so einem Container und in jedem Service bestünde ein weiterer Container, der wiederum die Partitionen enthält. Jedes neue Konfigurationsobjekt würde dann durchgereicht in den Baum bis ganz hinten und die Änderungen in den Daten würden einen Aufruf des Callbacks erzeugen, mit dem Hintergrund, dass auch eine Änderung im hinteren Teil des Baums ( z.B. auf Connectionebene ) einen Callback nach ganz außen zur Folge hätte.

    Ich habe aber den Gedanken eines allgemeinen Templates für Speicherung und Abgleich der Configs verworfen, weil gerade die innern Callbacks irgendwo gespeichert werden müssten, oder ich eben stets in jedem Aufruf von "update" eine gewissen Anzahl an Callbacks mitschleppen müsste.


Anmelden zum Antworten