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



  • 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?


  • Mod

    FlashBurn schrieb:

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

    Ich habe doch deutlich geschrieben, dass diese Implementierung den Overhead durch new vermeidet indem sie alle Objekte im Function Objekt konstruiert. ➡

    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.

    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.

    1. dynamic_cast hat ausschließlich das Potenzial zu werfen, wenn man zu einem Referenztyp konvertiert. Das ist hier nicht der Fall. 2. dynamic_cast ist nicht für das Aufrufen des targets nötig, du kannst dieses Feature auch komplett weglassen wenn du möchtest.


  • Hi!

    Ich habe mich auch mal dran versucht und damit den GCC 7.2 zum Absturz gebracht. Aber Clang 5 hat's geschafft. Es geht in Richtung "fast delegates", aber implementiert mit den neuen auto-Template-Parameter. 🕶

    Nachteile:

    • Unterstützt nur freie Funktionen und Objektmethoden

    Vorteile:

    • Kein new
    • Sehr klein (immer zwei Zeiger groß)
    • Der Compiler kann das gut optimieren

    Der Beispiel-Code sieht so aus:

    // Magische Definition von function<> und fun<> hier.
    
    #include <iostream>
    
    static void hello() {
    	std::cout << "Hello!\n";
    }
    
    struct strukt {
    	int member;
    	void increment() { member += 1; }
    	void show() const { std::cout << member << '\n'; }
    };
    
    int main() {
    	strukt s = { 42 };
    	function<void()> f = fun<&hello>();
    	f();
    	f = fun<&strukt::increment>(&s);
    	f();
    	f();
    	f();
    	f = fun<&strukt::show>(&s);
    	f();
    	return 0;
    }
    

    Hier ist der komplette Code. Das bekommt der Compiler auch krass optimiert: Es wird alles ge-inline-t.



  • FlashBurn schrieb:

    @Meep Meep

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

    nunja, in dem fall hat er bzw. ist es ein slot.

    du kannst ja ueber eine registrierungsfunktion mehrere signalobjekte in einen vector oder sonst wo reinstopfen. wenn dann das bestimmte event auftritt, dann gehst du einfach die liste durch und rufst alle auf.



  • @Columbo Ich bin zufällig über Deinen Code gestolpert. Sowas hatte ich schon länger für Anwendungen in kleinen Microcontrollern gesucht. Ich habe, nach einigen Anpassungen für c++11 toolchains, daraus eine kleine Arduino "Library" gemacht und würde die gerne auf gitHub stellen (Draft hier: https://github.com/luni64/staticFunctional) wenn Du nichts dagegen hast.

    Lizenz: MIT
    Ich würde dich als Author natürlich erwähnen.

    Passt das für Dich?


  • Mod

    @luni64 Einverstanden.


Anmelden zum Antworten