C++ Callbacks

Programmiersprachen, APIs, Bibliotheken, Open Source Engines, Debugging, Quellcode Fehler und alles was mit praktischer Programmierung zu tun hat.
Antworten
Benutzeravatar
Jonathan
Establishment
Beiträge: 2352
Registriert: 04.08.2004, 20:06
Kontaktdaten:

C++ Callbacks

Beitrag von Jonathan »

Einen schönen guten Abend zusammen.

Ich habe in meinem Projekt ein paar Objekte die auf bestimmte Ereignisse reagieren sollen. Aktuell verwende ich dazu boost::signals2 was viele nette Features beinhaltet allerdings auch ziemlich dick ist und die Kompilierzeit nach oben schraubt. Deshalb gucke ich mich gerade nach einer schlankeren Lösung um.

Nach ganz kurzer Recherche:
https://www.meetingcpp.com/blog/items/m ... splus.html
https://www.reddit.com/r/cpp/comments/b ... nalsslots/

Ansich scheint mir ein vector<shared_ptr<function<...>>> eine ganz gute Alternative. Das ist in wenigen Zeilen selber implementiert, man kann beliebig viele beliebige Callbacks registrieren (ob normale Funktionen oder Methoden konkreter Objekte) und dank shared_ptr muss man sich auch nicht groß darum kümmern, den Callback wieder zu löschen, wenn das zu reagierende Objekt gelöscht wird - man prüft einfach vor jedem Aufruf ob es dem Empfänger noch gibt und löscht ansonsten den Callback.

Es wird gerne mal angeführt, dass signals2 ja auch multithreadingsicher sei und so weiter. Ich vermute das bezieht sich auf Löschen des Empfängers, während alle Callbacks ausgeführt werden? (viel mehr Konflikte fallen mir gerade nicht ein). Das scheint für mich bis auf Weiteres keine Rolle zu spielen, ich würde also ungern dafür extra bezahlen. Andere Features sind explizite Aufrufreihenfolgemodellierung oder Verarbeitung von Rückgabewerten - brauch ich alles erstmal nicht.

Habt ihr irgendwelche Erfahrungen bezüglich Callbacksysteme in C++? Welche Lösung benutzt ihr und seid ihr damit glücklich?
Lieber dumm fragen, als dumm bleiben!
https://jonathank.de/games/
Benutzeravatar
Schrompf
Moderator
Beiträge: 4838
Registriert: 25.02.2009, 23:44
Benutzertext: Lernt nur selten dazu
Echter Name: Thomas Ziegenhagen
Wohnort: Dresden
Kontaktdaten:

Re: C++ Callbacks

Beitrag von Schrompf »

Benutze auch boost::signal2 bisher, würde auch gern weg davon kommen. Ist zwar schön gemacht, aber boost-üblich absurd kompliziert umgesetzt.

Entfernen aus dem Callback heraus brauch ich gelegentlich, kann ich empfehlen. Rückgabewert hab ich gelegentlich, aber solche Fälle kann man eigentlich auch mit nem plumpen "Hier, implementier dieses Interface" abhandeln. Was ich tatsächlich nie brauche, ist "mehrere Funktionen"
Früher mal Dreamworlds. Früher mal Open Asset Import Library. Heutzutage nur noch so rumwursteln.
D-eath
Beiträge: 51
Registriert: 28.08.2009, 19:37
Alter Benutzername: TrunkZ
Echter Name: Thomas

Re: C++ Callbacks

Beitrag von D-eath »

Für mein Eventsystem benutze ich einen simplen std::function-Ansatz, wie du ihn beschrieben hast. Das sieht dann wie folgt aus (ist evtl. nicht super toll gelöst, aber für mich reicht es vollkommen):

Code: Alles auswählen

			template<class T>
			using Slot = std::function<void(const T&)>;
			...
			std::map<E_EVENT_TYPE, std::vector<Slot<Event>>> _listeners;
		
...

			template<typename T>
			void addListener(Slot<T> listener)
			{
				_listeners[T::getEventType()].push_back(EventWrapper<T>(listener));
			}

			template<typename T>
			class EventWrapper
			{
			public:
				EventWrapper(Slot<T> callback) : _callback(std::move(callback)) {}
				void operator() (const Event& e)
				{
					_callback(static_cast<const T&>(e));
				}
			private:
				Slot<T> _callback;
			};

			template<typename T>
			void dispatch(Event& e)
			{
				for (const auto& listener : _listeners[T::getEventType()])
				{
					listener(static_cast<T&>(e));
				}
			}

			template<typename Callback, typename Object>
			static auto bindEventCallback(Callback callback, Object object)
			{
				return std::bind(callback, object, std::placeholders::_1);
			}
Ich übergebe verschiedene Unterobjekte von einer Eventklasse und caste die dann entsprechend im EventWrapper wieder um. Die Funktion bindEventCallback ist dann mein Template-Ersatz für ein entsprechendes Makro:

Code: Alles auswählen

_eventSystem->addListener<WindowResizeEvent>(EventSystem::bindEventCallback(&Engine::onWindowResizeEvent, this));
Was ich noch nicht implementiert habe, ist Löschen einzelner Callbacks, da bis dato noch nicht benötigt. Hier überlege ich, nen hashbasierten Ansatz zu fahren... ansonsten will ich mich bei meinem Entwicklungen ein bisschen an dem Signal- und Slot-Konzept von Qt orientieren.
Benutzeravatar
Jonathan
Establishment
Beiträge: 2352
Registriert: 04.08.2004, 20:06
Kontaktdaten:

Re: C++ Callbacks

Beitrag von Jonathan »

Ich habe mich nochmal dran gesetzt, hänge aber ein bisschen an den ganzen template-Parametern fest. Das System von D-eath ist schon ganz nett, aber ich möchte mehr Arten von Callbacks zulassen, nicht nur spezielle Callback-Objekte. Mein bisheriger Ansatz:

Code: Alles auswählen

template <typename ret_val, typename... params> class EventCallback
{
public:

	void Call(params... p)
	{
		for(auto& f : m_listener_list)
			f(p...);
	}

	// todo: at some point add return value so that listeners can be deactivated again
	void AddListener(std::function<ret_val(params...)> listener)
	{
		m_listener_list.emplace_back(listener);
	}
private:
	std::vector<std::function<ret_val(params...)>> m_listener_list;
};
Benutzt werden soll es unter anderem so:

Code: Alles auswählen

EventCallback<void, float> test;
test.AddListener([](float f) { cout << f << endl; });
test.AddListener([](float f) { cout << f*2 << endl; });
test.Call(9);
Das funktioniert auch bisher. Jetzt habe ich aber ein paar Probleme:

- Ich würde gerne als Templateparameter <void(float)> statt <void, float> verwenden (wie halt auch schon bei std::function), bekomme dann allerdings immer an der einen oder anderen Stelle Fehlermeldungen. Wie sieht die korrekte Syntax dafür aus.
- für <void, void> möchte die Call-Methode nicht kompilieren, ich schätze aber, das Problem löst sich zusammen mit dem ersten.
- Ich bin mir nicht ganz sicher, wie AddListener aussehen sollte. Prinzipiell möchte ich vermeiden, dass komsiche Kopien passieren, oder Callback doppelt in std::functions gewrappt werden. Wie löst man das effizient?

Sobald der bedarf entsteht, soll AddListener auch noch einen Identifier zurückgeben, mit dessen Hilfe man Callback auch wieder deaktivieren kann. Den kann man dann auch in einen unique_ptr mit custom deleter speichern, so dass Callback automatisch abgemeldet werden. Aber wie gesagt, später.
Lieber dumm fragen, als dumm bleiben!
https://jonathank.de/games/
Spiele Programmierer
Establishment
Beiträge: 426
Registriert: 23.01.2013, 15:55

Re: C++ Callbacks

Beitrag von Spiele Programmierer »

  1. Du kannst den Typ in Rückgabeparameter und Argumente mittels Template-Spezialisierung auflösen. Beispiel:

    Code: Alles auswählen

    template<typename T> class EventCallback {};
    template<typename ret_val, typename... params>
    class EventCallback<ret_val (params...)> { ... }
    
    Dann wird z.B. void(int,long,double) entsprechend zu ret_val = void und params = int, long, double.
  2. Wozu sollte <void, void> den deiner Meinung nach führen? Soll das wie in C void f(void) werden? Übergib doch einfach keine Argumente, typename... kann auch leer sein.
  3. Ich bin mir nicht ganz sicher ob ich die Frage richtig verstanden habe, aber spricht was gegen Folgendes?

    Code: Alles auswählen

    	void AddListener(std::function<ret_val(params...)> listener)
    	{
    		m_listener_list.push_back(std::move(listener));
    	}
    ?
    Oder die Callbacks direkt in der Liste konstruieren:

    Code: Alles auswählen

    	template<typename... ConstructionTs> void AddListener(ConstructionTs&&... construction_arguments)
    	{
    		m_listener_list.emplace_back(std::forward<ConstructionTs>(construction_arguments)...);
    	}
    Und zuletzt gibt es auch noch ein Mittelding das man zwei Überladungen mit const& und && anbietet.
Und wenn ich jetzt schon antworte, möchte ich noch kurz einwerfen dass ich ehrlich gesagt dem Vorhaben insgesamt bisschen skeptisch gegenüberstehe. Reicht ein simples Callback wirklich nicht aus? Ich frage das, wil ich früher auch mal sowas in der Richtung hatte (besonders für die GUI) aber es dann gestrichen habe. Meistens gibt es ja doch explizit irgendjemanden der das Objekt "besitzt" und damit alleiniges Anrecht darauf hat Callbacks zu setzen. In dem einen Ausnahmefall hatte ich dann einfach explizite ne Liste von Callbacks ohne Hilfsklasse gemacht. Ich denke mir eig. das das Pattern zu potentiell schwerer zu debuggenden Programmfluss verführt auch wenn dies vermeidbar wäre. In meinen Fall war das Problem das ich mir ursprünglich über die Abhängigkeiten nicht ganz im klaren war und damit gerechnet habe das jeder potentiell auf alles andere reagieren können muss.
Benutzeravatar
Jonathan
Establishment
Beiträge: 2352
Registriert: 04.08.2004, 20:06
Kontaktdaten:

Re: C++ Callbacks

Beitrag von Jonathan »

Hey, vielen Dank für die Antwort. Ich glaube fürs erste geht jetzt alles was ich wollte.

Zu 1: Ich glaube, genau die Syntax hatte ich gesucht. Ich habe zusätzlich noch die {} beim allgemeinen Template entfernt, damit man keine EventCallback mit Nicht-Funktions_Typ anlegen kann. Ich glaube man kann auch explizit eine Fehlermeldung werfen, aber so reicht es fürs erste auch.

Zu 2: Klappt jetzt auch direkt. Ich hatte da nur zwischendurch Fehlermeldungen als ich mit verschiedenen Syntaxen herumexperimentiert hatte.

Zu 3: Ja sowas wie die erste Lösung hatte ich geplant gehabt. Ich wollte nur vermeiden, dass das Callback eine Funktion aufruft die einfach nur eine weitere Funktion aufruft, aber danach sieht es eigentlich nicht aus.

Zum letzten Punkt: Was genau meinst du mit einem simplen Callback? Ich benutze momentan an verschiedenen Stellen boost::signal und wollte jetzt eine Leichtgewichtige Variante haben (die schneller kompiliert). Aus Performancesicht sollte ich vermutlich die ganze Engine umstellen um weniger von Callback abhängig zu sein (ich denke mit einem datenorientierten Aufbau könnte es insgesamt schneller laufen), aber das wäre viel mehr Arbeit. Ich sehe den konkreten Nachteil auch nicht ganz. Geht es nur um einen etwas komplizierteren Callstack beim Debuggen?
Meine Hauptmotivation bei dieser Klasse war es, nicht überall händisch ein std::vector<std::function>> einbauen zu müssen, sondern das nochmal in einer Klasse zu kapseln. Es macht natürlich keinen großen Unterschied, da hier wirklich nicht viel passiert, es ist aber m. M. n. etwas netter zu lesen und auch eine gute template-Fingerübung (offensichtlich^^).
Lieber dumm fragen, als dumm bleiben!
https://jonathank.de/games/
Spiele Programmierer
Establishment
Beiträge: 426
Registriert: 23.01.2013, 15:55

Re: C++ Callbacks

Beitrag von Spiele Programmierer »

Gegenüber Boost signal ist es bestimmt schonmal eine ordentliche Vereinfachung so wie ich Boost einschätzte. ^^

Mit einem simplen Callback habe ich std::function<...> gemeint. Die Performance ist natürlich nicht toll (bei std::function ja auch schon), aber eig. wollte ich auf die Wartbarkeit hinaus. So eine Event-Struktur braucht man ja nur, wenn man nicht weiß welche anderen Komponenten potentiell informiert werden sollen. z.B. um es etwas zu konkretisieren was ich meine:
Ein Fall wäre eine GUI bestehend aus einem Fenster und einem Kindfenster und man will dass das Elternfenster ebenfalls auf das Drücken eines Buttons im Kindfenster reagieren kann. Nur dann reicht ja ein simples std::function<...> nicht mehr aus und man will eine ganze Liste von aufzurufenden Funktionen so dass sowohl das Fenster als auch das Unterfenster gleichzeitig und unabhängig von einander auf das Ereignis reagieren können (und insbesondere das Unterfenster nichts davon wissen braucht). Und genau das sehe ich problematisch. Wenn man im Code an einem Punkt ist, wo man X beliebig viele unbekannte unterschiedliche Funktionen aufrufen will, dann sollte man sich doch Sorgen machen. Wenn möglich will man den Code doch so einfach wie möglich halten, so explizit wie möglich. Wenn das geht, ruft man eine Funktion direkt auf. Wenn das nicht geht, würde ich ein Callback machen. Und erst wenn das auch nicht geht so ein "Event". Um bei dem Beispiel mit dem Kindfenster zu bleiben, hier würde ich lieber ein zweites Callback einrichten das explizit für das Elternfenster ist und dann explizit vom Kind-Fenster bei dem Button-Callback aufgerufen wird. Damit kann man das Ereignis explizit weiterleiten und die Kapselung ist doch auch verbessert gegenüber einem direkt gemeinsam verwendeten "Event". Wie oft braucht man wirklich Events?
Bei mir wars so, dass ich irgendwie gedacht habe, Events wären automatisch besser als Callbacks. Man gewinnt ja (scheinbare) Flexibilität und muss sich (scheinbar) weniger Gedanken über die Strukturierung machen und überhaupt kennt man das ja aus anderen Sprachen wie C#.
Gerade in C++ muss man aber schon besonders aufpassen.
Man gerät leicht in eine Situation wo ein Event aufgerufen wird das eine Funktion beinhaltet dessen Objekt schon zerstört wurde. Events neigen dazu besonders tükisch im Bezug auf Objektlebenszeiten zu sein. Das Problem existiert mit etwas anderen Symptomen auch in C#. Ein dämliches nicht explizit entferntes Update-Event kann schnell verhindern, dass riesige Teile von eig. geschlossenen GUI-Elementen jemals gelöscht werden. Wenn man stattdessen explizit die Update-Funktion aufgerufen hätte, wäre das nicht passiert.
Benutzeravatar
Jonathan
Establishment
Beiträge: 2352
Registriert: 04.08.2004, 20:06
Kontaktdaten:

Re: C++ Callbacks

Beitrag von Jonathan »

Interessante Anregungen. Ich denke, zum Teil werde ich das so umsetzen:

- Eine Anwendung waren tatsächlich GUI-Klassen. Ich habe Buttons die etwas tun sollen, aber es stimmt schon, mir fallen keine sinnvollen Fälle ein in denen mehrere Stellen im Programm darauf reagieren müssen. Und wenn könnte ein entsprechender Eventhandler das ja auch weiterleiten. Dort werde ich dann also wohl statt boost::signal und statt EventCallback einfach eine std::function einsetzen.

- Ein anderer Teil vom Spiel geh hingegen explizit von solchen Listen aus. Ich habe Entities die Components haben und Components können sich für bestimmte Events registrieren (regelmäßiges Frame Update, Entity bewegt sich, usw.). Man hätte auch virtuelle Funktionen daraus machen können und Components die nicht auf ein Event reagieren wollen haben dann einfach leere Funktionen. Ich bin mir nicht ganz sicher was in welchen Situationen schlauer wäre. Ansonsten könnte man auch einen ganz anderen Aufbau wählen, aber dafür ist es jetzt ein wenig spät.
Lieber dumm fragen, als dumm bleiben!
https://jonathank.de/games/
Benutzeravatar
Jonathan
Establishment
Beiträge: 2352
Registriert: 04.08.2004, 20:06
Kontaktdaten:

Re: C++ Callbacks

Beitrag von Jonathan »

Ich teste gerade einen Prototypen des hier besprochenen Systems. Nachdem es nach langem Umschreiben wieder kompilierte gab es in der Spielwelt keine Bäume mehr und die Arbeiter arbeiten nicht mehr - aber das krieg ich schon noch in den Griff :D
Lieber dumm fragen, als dumm bleiben!
https://jonathank.de/games/
Benutzeravatar
xq
Establishment
Beiträge: 1581
Registriert: 07.10.2012, 14:56
Alter Benutzername: MasterQ32
Echter Name: Felix Queißner
Wohnort: Stuttgart & Region
Kontaktdaten:

Re: C++ Callbacks

Beitrag von xq »

Jonathan hat geschrieben: 24.03.2020, 21:50 Nachdem es nach langem Umschreiben wieder kompilierte gab es in der Spielwelt keine Bäume mehr und die Arbeiter arbeiten nicht mehr - aber das krieg ich schon noch in den Griff :D
Klingt nach einem Erfolg. Arbeiter haben alle Bäume gefällt und machen deshalb jetzt Pause.
War mal MasterQ32, findet den Namen aber mittlerweile ziemlich albern…

Programmiert viel in ⚡️Zig⚡️ und nervt Leute damit.
Benutzeravatar
Jonathan
Establishment
Beiträge: 2352
Registriert: 04.08.2004, 20:06
Kontaktdaten:

Re: C++ Callbacks

Beitrag von Jonathan »

Tjo, es lag an dem Connection-Objekt, dass alte Verbindungen automatisch löschen sollte. Ich habe jetzt gelernt, dass die default Move assignment operator Implementierung das Objekt einfach flach kopiert (weil der Zustand des alten Objektes nach dem move oft als undefiniert angenommen wird). Ich musste daraus jetzt ein richtiges swap machen, damit bei der Rückgabe einer neuen Verbindung die vorherige gelöscht wird und nicht direkt die neue.
Prinzipiell macht alles was im Hintergrund passiert schon Sinn, aber man muss auch wirklich wissen, was alles im Hintergrund passiert (und das ist mehr, als wonach es aussieht) um derlei Fehler zu vermeiden. C++ halt :D
Lieber dumm fragen, als dumm bleiben!
https://jonathank.de/games/
Benutzeravatar
Jonathan
Establishment
Beiträge: 2352
Registriert: 04.08.2004, 20:06
Kontaktdaten:

Re: C++ Callbacks

Beitrag von Jonathan »

Fürs Archiv hier die Variante die ich seit einiger Zeit benutze und in der ich länger keine Bugs mehr fixen musste:

Code: Alles auswählen

#pragma once

#include <functional>
#include <vector>
#include <memory>


/*

********* Overview *********

- use Connect() to add listener indefinitely
- use ConnectManaged() to add listener that is automatically disconnected
  when the connection object is destroyed
- Connection::Disconnect can be used to disconnects at a specific time
- To restart connection, use ConnectManaged() to assign a new Connection object


Supported Features:
- connecting or disconnecting listeners during Call

Unsupported Features:
- recursive calls (calling Call() during Call()) [throws Exception]
- Connecting more than UINT_MAX listeners during lifetime [undefined behaviour]

*/


template<typename T> class EventCallback;

template <typename ret_val, typename... params>
class EventCallback<ret_val(params...)>
{
public:

	void Call(params... p)
	{
		if(m_call_in_progress)
			throw Exception("call during call");

		//apply changes to listeners:
		for(auto& c : m_connect_list)
			Connect_apply(std::move(c));
		m_connect_list.clear();

		for(auto& c : m_disconnect_list)
			Disconnect_apply(c);
		m_disconnect_list.clear();

		// call all listeners
		m_call_in_progress = true;
		for(auto& f : m_listeners)
			f.fun(p...);
		m_call_in_progress = false;
	}

	void Connect(std::function<ret_val(params...)> listener)
	{
		m_connect_list.emplace_back(std::move(listener), m_IdCounter++);
	}

	void Disconnect(unsigned int Id)
	{
		m_disconnect_list.push_back(Id);
	}


	class Connection
	{
	public:
		Connection() :
			m_event(nullptr),
			m_Id(333333)
		{
		}
		Connection(EventCallback* event, unsigned int Id) :
			m_event(event),
			m_Id(Id)
		{
		}
		~Connection()
		{
			Disconnect();
		}

		bool isActive()
		{
			return m_event != nullptr;
		}

		void Disconnect()
		{
			if(m_event)
			{
				m_event->Disconnect(m_Id);
				m_event = nullptr;
			}
		}

		// make moveable but not copyable:
		Connection(const Connection&) = delete;
		Connection& operator=(const Connection&) = delete;
		Connection(Connection&&) = default;
		Connection& operator=(Connection&& v)
		{
			// the default implementation does just a copy
			// but we need the old object to be in a defined state
			// so that the connection is properly disconnected.
			std::swap(m_event, v.m_event);
			std::swap(m_Id, v.m_Id);
			return *this;
		};
	private:
		EventCallback* m_event;
		unsigned int m_Id;
	};
	
	// when the connection object is destroyed, the connection is cancelled
	Connection ConnectManaged(std::function<ret_val(params...)> listener)
	{
		m_connect_list.emplace_back(std::move(listener), m_IdCounter++);
		return Connection(this, m_IdCounter - 1); // because it was already incremented
	}

private:
	struct Entry
	{
		Entry(std::function<ret_val(params...)> f, unsigned int i) :
			fun(f),
			Id(i)
		{
		}

		std::function<ret_val(params...)> fun;
		unsigned int Id;
	};


	void Connect_apply(Entry&& listener)
	{
		if(m_call_in_progress)
			throw Exception("connect during call");
		m_listeners.emplace_back(std::move(listener));
	}

	void Disconnect_apply(unsigned int Id)
	{
		if(m_call_in_progress)
			throw Exception("disconnect during call");
		for(auto i = m_listeners.begin(); i != m_listeners.end();)
		{
			if(i->Id == Id)
				i = m_listeners.erase(i);
			else
				i++;
		}
	}

	unsigned int m_IdCounter = 0;
	std::vector<Entry> m_listeners;

	// to support connecting / disconnecting listeners during Call() we need to do it lazy
	std::vector<Entry> m_connect_list;
	std::vector<unsigned int> m_disconnect_list;

	bool m_call_in_progress = false; // to check if listener list is edited during call
};
Lieber dumm fragen, als dumm bleiben!
https://jonathank.de/games/
Antworten