Funktion mit std::function überladen
-
Hi!
Ich will eine Funktion anhand ihres Parameters überladen, welcher in beiden Fällen eine std::function ist:
my_func(std::function<void(int, double)> f) { f(4, .4); } // #1 my_func(std::function<void(int&, double)> f) { int i; f(i, .4); } // #2 ... my_func([](int, double) { }); // ruft #1 auf my_func([](int&, double) { }); // ruft #2 aufIch erhalte (wegen ganzen type erasure-Zeug mit Template-Konstruktor von std::function):
ambigious function callFrage: Was tun? Ich will Sachen wie explizit casten im Interface vermeiden. Gibt es irgendeinen SFINAE/TMP-Hack, der es mir erlaubt, das gewünschte Resultat mit genau diesem Interface zu erhalten?
-
Was willst du erreichen? Warum sollen die Fälle unterschieden werden?
Je nach Signatur ist ja die Semantik eine andere. Davon hängt es dann ab, ob ein temporäres Objekt Sinn macht...
-
Selbst mit SFINAE ist es nicht ganz einfach, da man ja beide Funktionen mit int& aufrufen kann. Man müsste dann die Parameter einzeln mit std::is_same oder so checken.
Wieso brauchst du das denn?
-
Nexus schrieb:
Was willst du erreichen? Warum sollen die Fälle unterschieden werden?
Naja du siehst ja schon im Beispiel-Code, dass sich die Überladungen unterscheiden (müssen). Noch dramatischer werden die Unterschiede in beiden Fällen, wenn die functions asynchron in unterschiedlichen Threads laufen (und dabei, um Synchronisation zu sparen, bestimmte Objekte die Ownership von Thread zu Thread reichen).
In etwa so:
type object; object.do_somewhen([](type t) { ... }); // object wird ungültig nach dem Call, übergibt ownership an den neuen Thread, der den Lambda ausführt; der Caller muss sich keine Gedanken mehr um object an dieser Stelle machen vs. type object; object.do_somewhen([](type& t) { ... }); // object bleibt dem Caller erhalten, er kann dadurch mehrere do_somewhen aufrufen, allerdings muss der Caller-Thread nun sich um die Gültigkeit von object kümmern, bis der Lambda aufgerufen wurde.
-
Mach es explizit. Solche "cleveren" Features, die hinter den Kulissen eine ganz andere Semantik haben und möglicherweise unerwartete Dinge tun, führen schnell zu sehr mühsamen Bugs. C++ ist im Gegensatz zu anderen Sprachen immer ziemlich aussagekräftig, wenn es um Besitzverhältnisse geht.
Aber ist die Fallunterscheidung wirklich unbedingt nötig? Kann dem Thread nicht immer (oder nie) Besitz übergeben werden?
-
Nexus schrieb:
Mach es explizit.
Hm. Da kann ich entweder die Funktionen umbenennen (was ich unschön finde, denn sie machen im Prinzip das selbe) oder den Typ aufspalten (was auch einen Haken hat, siehe unten).
[Das wird leider etwas mehr Text jetzt...]
Nexus schrieb:
Aber ist die Fallunterscheidung wirklich unbedingt nötig? Kann dem Thread nicht immer (oder nie) Besitz übergeben werden?
Damit suggerierst du wohl, dass ich aus einem zwei Typen machen soll. Aber das wäre schade.
Fall A: object ist thread-safe (enthält also z.B. einen non-moveable Mutex-Member).
Ein Aufruf vom Typtype<thread_safe> object; object.do_somewhen([](type<thread_safe> same_object) { ... });würde dann wenig Sinn ergeben, da type<thread_safe> ebenfalls nicht moveable ist. Der Besitz wäre an einen Thread gekettet (der sich darum kümmern muss, das Objekt zu halten), die Besitzverhältnisse sind aber dafür einfach (da konstant).
Außerdem kann man jetzt threadsicher das machen:container<type<thread_safe>> container; ... container.front().do_somewhen([](type<thread_safe>& same_object) { ... }); container.front().do_somewhen([](type<thread_safe>& same_object) { ... });Mehrere Threads greifen nun thread-sicher irgendwann auf das Objekt zu. Der Besitzer-Thread ist dafür verantwortlich, das Objekt irgendwann zu löschen (insbesondere nachdem kein do_somewhen mehr läuft).
Diese Synchronisation hat seinen Preis, und es kommt evtl. noch eine Synchronisation des containers dazu. Das ist allerdings manchmal garnicht erwünscht, wenn z.B. garantiert ist, dass nur ein Thread Zugriff auf das Objekt hat:Fall B: Das Objekt ist per se nicht thread-safe. Wenn ich keine Thread-Saftety brauche, will ich dafür auch nicht bezahlen (mit Zeit + zusätzlicher Objektgröße durch Mutex-Member). Wenn ich bei einem asynchronen Call die Ownership gleich mit weggebe, bin ich vor einer Race-Condition durch eine Exception geschützt:
type<thread_unsafe> object; object.do_somewhen([](type<thread_unsafe> o) { ... }); // Ownership wandert an einen Thread (beim Aufruf von do_somewhen, nicht erst beim Aufruf des Lambdas irgendwann) object.do_somewhen([](type<thread_unsafe> o) { ... }); // daher ist so ein Aufruf bereits ungültig und führt zu einer Exception, da nur ein Thread Zugriff und Besitz des nicht-synchronisierten Objektes haben sollAußerdem wirkt RAII hier wunderbar:
objectlebt nur solange, wie eine asynchrone Operation auf dem Objekt wirkt (da es dem Objekt die Ownership/Ressourcen entzieht und am Scope-Ende nur ein ungültiges Objekt zerstört wird). Dadurch muss der Client sich nicht selber um das Buchhalten der Objekte kümmern (und den Container u.U. auch noch synchronisieren). Sobald er keine Operation mehr auf dem Objekt ausführt, wird es am Scope-Ende gelöscht. Nur ein Thread besitzt das Objekt. Nur ein Thread kann eine asynchrone Operation auf dem Objekt ausführen, Beispiel:type<thread_unsafe> object; // übergebe Ownership: object.do_somewhen([](type<thread_unsafe> o) { // übergebe Ownership: o.do_somethring_different_somewhen([](type<thread_unsafe> oo) { // keine neue Operation auf oo, also wird das Objekt am Ende des Scopes gelöscht }); });Ich spare mir also Synchronisation vom Objekt und Buchhalten + Synchronisation der Buchhaltung.
Soweit klingt es ja so, als ob ich also wunderbar do_somewhen mit Value-Semantik (also mit std::function-Parameter mit call-per-value) in einem nicht-thread-sicheren Typen und mit Referenz-Semantik an einen thread-sicheren Typen definieren kann.
Allerdings ginge dann nicht mehr das:
type<thread_unsafe> object; object.do_somewhen([](type<thread_unsafe>& o) { ... }); object.do_somewhen([](type<thread_unsafe>& o) { ... }); // potentielle Race-Condition! Der Client hat nun die Aufgabe, object am Leben zu erhalten, bis alle do_somewhens fertig sind + er muss innerhalb vom Lambda Zugriffe auf das Objekt selber synchronisierenDiese Version verlangt dem Client also höchste Aufmerksamkeit ab, aber diese Version braucht er nur, wenn er selber (u.U. schneller oder plattformabhängig) synchronisieren will als in der Standard-Synchronisierung von
type<thread_safe>(die vermutlich std::mutex nutzen würde).Also thread-safe-Objekt kann nur Referenz-Semantik, aber non-thread-safe kann theoretisch Referenz- und Value-Semantik. Trennen des Typs hat also hier auch seinen Preis, da der Client dann weniger flexibel ist.
Hätte am Anfang nicht gedacht, dass es so schwierig ist, Flexibilität mit "you only pay for what you use" zu vereinigen.
-
Verwende zwei verschiedene Namen.