std::function und std::bind durch einfachere eigene Variante ersetzen (signal/slot)



  • Bisher benutze ich in meinem Code std::function und std::bind um Callbacks nutzen zu können.

    Allerdings läuft der finale Code auf einem Embedded System und daher möchte/muss ich auf new verzichten und das ist bei std::bind ja nicht sicher gestellt. Zumal zumindest laut Debugger der Overhead auch nicht zu knapp ist.

    Ich zerbreche mir jetzt schon den ganzen Tag den Kopf (und habe versucht die Templates von std::function und std::bind zu verstehen) um eine einfache eigene Lösung zu entwickeln.

    Meine Idee war, std::function ist ein Objekt welches einen Funktionszeiger und einen Objektzeiger speichert, weil mehr brauche ich nicht.

    In der Theorie ist es ja ganz einfach, das Objekt wird entweder nur mit einem Funktionszeiger initialisiert (und der Objektzeiger ist nullptr) oder mit einem Funktionszeiger und einem Objektzeiger.
    Der Funktionsaufrufoperator ("()") wird überladen und in der Funktion gucke ich einfach nur ob der Objektzeiger == nullptr ist und dann brauche ich nur den Funktionszeiger aufrufen und ansonsten muss ich den Memberfunktionszeiger des Objekts aufrufen.

    Mein erster "Versuch" schaut so aus:

    template <typename functionType>
    class smartFunctionMemberPointer_Base
    {
    public:
    	smartFunctionMemberPointer_Base() {}
    	~smartFunctionMemberPointer_Base() {}
    
    	virtual decltype(functionType::type) operator()(...) = 0;
    };
    
    template <typename functionType>
    class smartFunctionPointer_Impl : public smartFunctionMemberPointer_Base<functionType>
    {
    private:
    	functionType* _function;
    public:
    	smartFunctionPointer_Impl(functionType* function)
    		: _function(function) {}
    	~smartFunctionPointer_Impl() {}
    
    	virtual decltype(functionType::type) operator()(...)
    	{
    		return _function(...);
    	}
    };
    
    template <typename functionType, class objectType>
    class smartFunctionMemberPointer_Impl : public smartFunctionMemberPointer_Base<functionType>
    {
    private:
    	functionType*	_function;
    	objectType*		_object;
    public:
    	smartFunctionMemberPointer_Impl(functionType*	function,
    									objectType*		object)
    		: _function(function),
    		_object(object) {}
    	~smartFunctionMemberPointer_Impl() {}
    
    	virtual decltype(functionType::type) operator()(...)
    	{
    		return _object->*_function(...);
    	}
    };
    

    Allerdings sind so beide Objekte nicht gleich groß. Auch weiß ich nicht wie ich dem Compiler das mit den Parametern sage.



  • Leider kenne ich damit recht wenig aus, aber zwei Fragen:

    Brauchst du wirklich std::function und dessen type erasure? Vermutlich ja, wenn ich sehe, dass du das für member-Funktionen und non-member-Funktionen haben willst. Aber std::function selbst hat natürlich auch Overhead (ich weiß nicht, wie das implementiert ist, aber naiv würde ich darin auch ein new vermuten).

    Hast du mal probiert, statt bind ein Lambda zu nehmen? Die werden häufig besser optimiert. Du kannst ja auch ein Lambda nehmen, das den this-Zeiger captured und dann darin eine Objekt-Funktion aufrufen.



  • Die Variante von Lambdas kannte ich noch gar nicht und es scheint auch zu funktionieren (laut Unit Tests), aber auch das Lambda führt zu einem new 😞



  • Ein Debugger zum Messen des Overheads?



  • manni66 schrieb:

    Ein Debugger zum Messen des Overheads?

    Naja, so lange ich beim GCC -O0 verwende wird genau der Code rauskommen, der auch beim Debuggen rauskommt und wer garantiert mir wie sehr und was genau der Compiler wegoptimiert?

    Nur als Info am Rande, die Callbacks sind im worst-case sehr zeit kritisch und bei der Menge an Callbacks die im worst-case gemacht werden, zählt halt jede Instruktion.



  • FlashBurn schrieb:

    zählt halt jede Instruktion.

    Ja, und deswegen schaltet man den Optimizer ab. Pfiffig...



  • ich hab mir fuer events ueber dll grenzen mal eine klasse zusammen gebaut, die recht klein und flott ist:

    template<class RET = void>
    class signal
    {
       public:
          using return_type = RET;
          using callee_signature = auto (__stdcall *)(void*) -> return_type;
    
          template<class T>
          using extern_signature = auto (__stdcall *)(T*) -> return_type;
    
          signal(void) noexcept : function(nullptr), object(nullptr)
          {
          }
    
          template<class T>
          signal(const extern_signature<T> &t_func, void *t_obj = nullptr) noexcept : function(reinterpret_cast<callee_signature>(t_func)), object(t_obj)
          {
          }
    
          signal(const signal &other) noexcept : function(other.function), object(other.object)
          {
          }
    
          signal(signal &&other) noexcept : function(other.function), object(other.object)
          {
          }
    
          ~signal(void) noexcept
          {
          }
    
          auto operator=(const signal &other) noexcept -> signal&
          {
             function = other.function;
             object = other.object;
    
             return *this;
          }
    
          auto operator()(void) noexcept -> return_type
          {
             return function(object);
          }
    
       private:
          callee_signature function;
          void *object;
    }; /* class signal */
    

    funktioniert natuerlich nur mit freien und static member funktionen. aber dafuer habe ich den ersten parameter. wenn es eine statische member funktion ist, dann wird der this-pointer immer an der ersten stelle uebergeben. damit kann man dann in der statischen funktion auch nicht-statische funktionen eines objektes aufrufen. ansonsten muss man nur die parameterliste von den 2 signaturen anpassen.



  • @Meep Meep

    Wenn ich deinen Code nicht vollständig falsch verstanden habe, dann kann er aber nur einen Empfänger benachrichtigen!?

    @all

    Nach Meep Meep seinem Code bin ich dann drauf gekommen, dass ich bei mir das Signal-Slot Konzept mit Hilfe von std::function und std::bind umgesetzt habe.

    Dies würde ich gerne, wie schon geschrieben, gegen etwas eigenes (oder offenem anderen Code) ersetzen, was die Standard-Library nicht benutzt.

    Ich wollte ganz einfach anfangen und nur ein Signal haben, welches alle seine bei Ihm registrierten Slots durchgeht und den Handler ausführt.
    Allerdings bekomme ich das schon nicht mit meinen bescheidenen Template Kenntnissen hin.

    Die Anforderungen sind im Prinzip recht einfach, ein Slot ist entweder eine template Klasse die einen Funktionspointer speichert oder eine template Klasse die einen Memberfunktionspointer und einen Objektpointer speichert. Das Signal kann Slots beider Varianten "aufnehmen" (wobei das Signal nur einen Pointer auf das Erste Elemente einer einfach verketteten Liste speichert) und geht diese Liste durch und ruft die Handler der Slots auf.

    In Pseudocode formuliert:

    class ISlot
    {
    private:
        *ISlot _next;
    public:
        ISlot()
        : _next(nullptr) {}
        ~ISlot() {}
    
        virtual void operator()(ARGS) = 0;
    
        ISlot* getNext()
        {
            return _next;
        }
    
        void setNext(ISlot* next)
        {
            _next = next;
        }
    };
    
    class SlotFunctionPointer : public ISlot
    {
    private:
        FunctionPointer* _function;
    public:
        SlotFunctionPointer(FunctionPointer* function)
            : _function(function) {}
        ~SlotFunctionPointer() {}
    
        virtual void operator()(ARGS)
        {
            _function(ARGS);
        }
    };
    
    class SlotFunctionMemberPointer : public ISlot
    {
    private:
        FunctionMemberPointer* _function;
        ObjectPointer* _object
    public:
        SlotFunctionPointer(FunctionMemberPointer* function, ObjectPointer* object)
            : _function(function),
            _object(object) {}
        ~SlotFunctionPointer() {}
    
        virtual void operator()(ARGS)
        {
            object->*_function(ARGS);
        }
    };
    
    class Signal
    {
    private:
        ISlot* _head;
    public:
        Signal()
            : _head(nullptr) {}
        ~Signal() {}
    
        void operator()(ARGS)
        {
            ISlot* walker = _head;
    
            while(walker != nullptr)
            {
                walker();
    
                walker = walker->getNext();
            }
        }
    
        void connectSlot(ISlot* slot)
        {
            slot->setNext(_head);
    
            _head = slot;
        }
    };
    

    Allerdings reichen meine Template Kenntnisse nicht dafür aus wie ich den Pseudo-Code in funktionierenden C++11/14 Code bekomme.

    Daher bin ich für jeden Tipp/Link/Vorschlag dankbar.



  • FlashBurn schrieb:

    Dies würde ich gerne, wie schon geschrieben, gegen etwas eigenes (oder offenem anderen Code) ersetzen, was die Standard-Library nicht benutzt.

    Warum das denn? Zur Übung? In der tatsächlichen Anwendung würde ich dir dringend dazu raten, die standard Bibliothek zu benutzen. Die ist vielfach getestet und daher wahrscheinlich fehlerfreier als dein Code und höchst wahrscheinlich auch perfomanter bzw. vom Compiler besser zu optimieren.



  • Es gibt schon Gründe, warum man eine nicht-STL Lösung benutzen möchte. std::function Objekte sind zB nicht vergleichbar, das macht das Handling unter Umständen schwierig, zB wenn man einen vector pflegt und wissen möchte, ob eine Callback bereits registriert ist oder nicht. Oder einen bestimmten Callback löschen möchte.

    Vielleicht hilft dir das hier weiter:
    Impossibly fast delegates C++03 und
    Impossibly fast delegates C++11



  • Schlangenmensch schrieb:

    Warum das denn?

    Wie schon geschrieben, kann ich bei Verwendung von std::function und std::bind nicht ausschließen das new benutzt wird und genau das will ich verhindern.

    Der Code läuft auf einem Embedded System und dort habe ich nur wenig bis keinen Speicher für new/malloc zur Verfügung.

    Edit::

    @ DocShoe

    Danke, die sehe ich mir mal an.



  • Ah, es geht darum speziell std::function und ähnliches nicht aus der std zu benutzen. Ich hatte die Aussage irgendwie so verstanden, dass du gar keine Teile aus der std benutzen möchtest.

    Also ist das Ziel, sowas zu implementieren ohne das Speicher auf dem Heap benötigt wird?



  • Schlangenmensch schrieb:

    Ah, es geht darum speziell std::function und ähnliches nicht aus der std zu benutzen. Ich hatte die Aussage irgendwie so verstanden, dass du gar keine Teile aus der std benutzen möchtest.

    Also ist das Ziel, sowas zu implementieren ohne das Speicher auf dem Heap benötigt wird?

    Genau 😃



  • Bisher habe ich:

    template<typename Signature>
    class ISlot;
    
    template<typename RET, typename... ARGS>
    class ISlot<RET(ARGS...)>
    {
    private:
    	ISlot* _next;
    public:
    	ISlot()
    		: _next(nullptr) {}
    	~ISlot() {}
    
    	virtual void handler(ARGS...) = 0;
    
    	ISlot* getNext()
    	{
    		return _next;
    	}
    
    	void setNext(ISlot* next)
    	{
    		_next = next;
    	}
    };
    
    template<typename Signature>
    class Signal;
    
    template<typename RET, typename... ARGS>
    class Signal<RET(ARGS...)>
    {
    private:
    	ISlot<void(ARGS...)>* _head;
    public:
    	Signal()
    		: _head(nullptr) {}
    	~Signal() {}
    
    	void operator()(ARGS... args)
    	{
    		ISlot<void(ARGS...)>* walker = _head;
    
    		while (walker != nullptr)
    		{
    			walker->handler(args...);
    
    			walker = walker->getNext();
    		}
    	}
    
    	void connectSlot(ISlot<void(ARGS...)>* slot)
    	{
    		slot->setNext(_head);
    
    		_head = slot;
    	}
    };
    

    Wobei mir nicht 100% klar ist warum es funktioniert 😉

    Jetzt muss ich noch die Slots für Funktionspointer und für Memberfunktionspointer hinbekommen und ich habe genau das was ich haben wollte.



  • Die templates für die Funktionspointer sehen so aus:

    template<typename Signature>
    class SlotFunctionPointer;
    
    template<typename RET, typename... ARGS>
    class SlotFunctionPointer<RET(ARGS...)> : public ISlot<void(ARGS...)>
    {
    private:
    	void (*_function)(ARGS...);
    public:
    	SlotFunctionPointer(void(*function)(ARGS...))
    		: _function(function) {}
    	~SlotFunctionPointer() {}
    
    	virtual void handler(ARGS... args)
    	{
    		_function(args...);
    	}
    };
    

    Jetzt nur noch die Templates für Memberfunktionspointer, aber ich weiß jetzt schon, das wird deutlich kniffliger.



  • Die templates für die Memberfunktionspointer sehen so aus:

    template<typename Signature, Signature>
    class SlotFunctionMemberPointer;
    
    template<typename T, typename RET, typename... ARGS, RET (T::*F)(ARGS...)>
    class SlotFunctionMemberPointer<RET(T::*)(ARGS...), F> : public ISlot<void(ARGS...)>
    {
    private:
    	T* _object;
    public:
    	SlotFunctionMemberPointer(T* object)
    		: _object(object) {}
    	~SlotFunctionMemberPointer() {}
    
    	virtual void handler(ARGS... args)
    	{
    		(_object->*F)(args...);
    	}
    };
    

    Ein Testprogramm sieht so aus:

    class TestClassSignal
    {
    public:
    	Signal<void(int, int)> signalTest;
    
    	TestClassSignal() {}
    	~TestClassSignal() {}
    };
    
    void testHandler(int a, int b)
    {
    	std::cout << a << " " << b << std::endl;
    }
    
    class TestClassSlot
    {
    public:
    	TestClassSlot()
    		: slot(this) {}
    
    	void handler(int a, int b)
    	{
    		std::cout << (a * 10) << " " << (b * 10) << std::endl;
    	}
    
    	SlotFunctionMemberPointer<void (TestClassSlot::*)(int, int), &TestClassSlot::handler> slot;
    };
    
    int main()
    {
    	TestClassSignal						testSignal;
    	SlotFunctionPointer<void(int, int)> testSlot1(&testHandler);
    	TestClassSlot						testSlot2;
    
    	testSignal.signalTest.connectSlot(&testSlot1);
    	testSignal.signalTest.connectSlot(&testSlot2.slot);
    	testSignal.signalTest(1, 2);
    
        return 0;
    }
    

    Mal sehen, vielleicht bekomme ich es morgen noch hin die Templates zu verbessern, das sie nicht mehr so viele Parameter brauchen.


  • Mod

    Polymorphe Wrapper können auch mit Stack Speicher auskommen. Hier eine Demo mit Memberfunktionszeigern. Allerdings ist die Function durch die ganzen pointer to member ziemlich riesig. 80 Byte bei 16 Byte payload. Mit gewöhnlichen Funktionszeigern und statischen Memberfunktionstemplates kann das natürlich gedeckelt werden.

    Falls dir eine zusätzliche Indirektion nichts ausmacht, bist du mit einer polymorphen Lösung aber vielleicht besser bedient:

    #include <functional>
    #include <cstddef>
    
    template <typename Signature, std::size_t N>
    class Function;
    template <typename R, typename... Args, std::size_t N>
    class Function<R(Args...), N>
    {
      std::aligned_storage_t<N> _storage;
    
      struct InvokerBase {
        virtual R operator()(void const*, Args...) const = 0;
        virtual void destroy(void*) = 0;
        virtual void copy(void const*, void*, void*) const = 0;
        virtual void move(void*, void*, void*) const = 0;
        virtual std::type_info const& type() const noexcept;
      };
    
      template <typename F>
      struct Invoker : InvokerBase {
        static_assert(sizeof(F) <= N);
    
        R operator()(void const* p, Args... args) const override {
          return std::invoke(*std::launder((F const*)p), std::forward<Args>(args)...);
        }
        void destroy(void* p) override {
          std::launder((F*)p)->~F();
        }
        void copy(void const* s, void* d, void* invoker_d) const override {
          new (d) F(*std::launder((F const*)s));
          new (invoker_d) Invoker;
        }
        void move(void* s, void* d, void* invoker_d) const override {
          new (d) F(std::move(*std::launder((F*)s)));
          new (invoker_d) Invoker;
        }
        std::type_info const& type() const noexcept {
          return typeid(F);
        }
      };
    
      std::aligned_storage_t<sizeof(Invoker<char>), alignof(Invoker<char>)> _invoker_storage;
      InvokerBase const& _invoker_base() const noexcept  {
        return *std::launder((InvokerBase*)&_invoker_storage); }
      InvokerBase& _invoker_base() noexcept  {
        return *std::launder((InvokerBase*)&_invoker_storage); }
      bool _empty;
    
      void _clear() {
        if (!empty()) {
          _invoker_base().destroy(&_storage);
          _empty = true;
        }
      }
    
      template <typename F>
      static const bool _is_callable =
        std::is_convertible_v<std::invoke_result_t<F, Args...>, R>;
    
      template <typename F>
      static const bool _participate = _is_callable<F>
                                    && !std::is_same_v<std::decay_t<F>, Function>;
    
      template <typename>
      struct _is_function_spec : std::false_type{};
      template <typename X, std::size_t M>
      struct _is_function_spec<Function<X, M>> : std::true_type{};
    
      // Assumes that *this is empty.
      template <typename F>
      void _emplace(F&& f) {
        using T = std::decay_t<F>;
        if constexpr(std::is_pointer_v<T> || std::is_member_function_pointer_v<T> || _is_function_spec<T>{})
          if (!f)
            return;
    
        static_assert(sizeof(Invoker<F>) <= sizeof(_invoker_storage));
        new (&_storage) F(std::forward<F>(f));
        new (&_invoker_storage) Invoker<F>;
        _empty = false;
      }
    
    public:
    
      using result_type = R;
    
      template <typename F>
      F const* target() const {
        return dynamic_cast<Invoker<F>>(_invoker_base())?
          std::launder(reinterpret_cast<F const*>(&_storage)) : nullptr;
      }
      template <typename F>
      F* target() {
        return const_cast<F*>(std::as_const(*this).template target<F>());
      }
    
      std::type_info const& target_type() const noexcept {
        _invoker_base()->type();
      }
    
      bool empty() const noexcept {
        return _empty;
      }
    
      operator bool() const noexcept {
        return !empty();
      }
    
      ~Function() {
        _clear();
      }
    
      Function() noexcept = default;
      Function(std::nullptr_t) noexcept {}
    
      Function(Function&& other) : _empty(other.empty()) {
        if (!empty())
          other._invoker_base()->move(&other._storage, &_storage, &_invoker_storage);
      }
    
      Function(Function const& other) : _empty(other.empty()) {
        if (!empty())
          other._invoker_base().copy(&other._storage, &_storage, &_invoker_storage);
      }
    
      template <typename F,
                typename = std::enable_if_t<_participate<F>>>
      Function(F&& f) : _empty(false) {
        _emplace(std::forward<F>(f));
      }
    
      Function& operator=(Function const& other) {
        _clear();
        _empty = other.empty();
        if (!empty())
          other._invoker_base().copy(&other._storage, &_storage, &_invoker_storage);
        return *this;
      }
    
      Function& operator=(Function&& other) {
        _clear();
        _empty = other.empty();
        if (!empty())
          other._invoker_base().move(&other._storage, &_storage, &_invoker_storage);
        return *this;
      }
    
      Function& operator=(std::nullptr_t) {
        _clear();
        return *this;
      }
    
      template <typename F,
                typename = std::enable_if_t<_participate<F>>>
      Function& operator=(F&& f) {
        _clear();
        _emplace(std::forward<F>(f));
        return *this;
      }
    
      template <typename F>
      Function& operator=(std::reference_wrapper<F> f) {
        _clear();
        _emplace(f.get());
        return *this;
      }
    
      void swap(Function& other) {
        std::swap(*this, other); // Best that can be done.
      }
    
      R operator()(Args... args) const {
        return _invoker_base()(&_storage, std::forward<Args>(args)...);
      }
    };
    
    template <typename R, typename... Args, std::size_t N>
    bool operator==(Function<R(Args...), N> const& f, std::nullptr_t) noexcept {
      return !f;; }
    template <typename R, typename... Args, std::size_t N>
    bool operator==(std::nullptr_t, Function<R(Args...), N> const& f) noexcept {
      return !f; }
    template <typename R, typename... Args, std::size_t N>
    bool operator!=(Function<R(Args...), N> const& f, std::nullptr_t) noexcept {
      return f; }
    template <typename R, typename... Args, std::size_t N>
    bool operator!=(std::nullptr_t, Function<R(Args...), N> const& f) noexcept {
      return f; }
    
    template <typename R, typename... Args, std::size_t N>
    void swap(Function<R(Args...), N> &lhs, Function<R(Args...), N> &rhs) {
      lhs.swap(rhs);
    }
    
    #include <iostream>
    int main() {
      Function<int(int), 16> f([] (int i) {return i*i;});
      auto g = f;
      g = f;
      std::cout << g(5);
      std::cout << "\nSize of Function: " << sizeof(f);
    }
    

    Jetzt haben wir bei 16 Byte payload 32 Byte Function . Viel besser wird's nicht.


  • Mod

    Das Problem war schnell gefunden. Anscheinend ist die Standardausrichtung von aligned_storage 32? Invoker braucht nur 16. Damit haben wir 32 Byte.



  • @Arcoth

    Ich muss zugeben das ich nicht viel von dem Code verstehe, insbesondere nicht wozu man den braucht.

    Für mich ist er außerdem nicht nutzbar, da ein dynamic_cast benutzt wird und der eine Exception werfen kann, welche auf meinem Embedded System auch nicht zur Verfügung stehen.

    Mich würde allerdings interessieren, welchen Vorteil deine Version bietet?



  • FlashBurn schrieb:

    Für mich ist er außerdem nicht nutzbar, da ein dynamic_cast benutzt wird und der eine Exception werfen kann

    Aus Interesse: in welchen Fällen kann denn dynamic_cast Exceptions werfen?


Log in to reply