Hintergrund
Ich mag die C ++ Sprache. Ich würde sogar sagen, dass dies meine Lieblingssprache ist. Darüber hinaus verwende ich für meine Entwicklung .NET-Technologien, und viele der darin enthaltenen Ideen sind meiner Meinung nach einfach erstaunlich. Nachdem ich auf die Idee gekommen war, wie man Reflexionsmittel und dynamische Funktionsaufrufe in C ++ implementiert. Ich wollte wirklich, dass C ++ einen solchen CLI-Vorteil hat, wie das Aufrufen eines Delegaten mit einer unbekannten Anzahl von Parametern und deren Typen. Dies kann beispielsweise nützlich sein, wenn nicht im Voraus bekannt ist, welche Datentypen die Funktion aufgerufen werden muss.
Natürlich ist eine vollständige Nachahmung der Delegierten zu kompliziert, daher zeigt dieser Artikel nur die allgemeine Architektur der Bibliothek und die Lösung einiger wichtiger Probleme, die auftreten, wenn es um etwas geht, das nicht direkt von der Sprache unterstützt wird.
Aufrufen von Funktionen mit einer unbestimmten Anzahl von Parametern und unbekannten Typen während der Kompilierung
Dies ist natürlich das Hauptproblem bei C ++, das nicht so einfach zu lösen ist. Natürlich gibt es in C ++ ein Tool, das von C -
varargs geerbt wurde , und höchstwahrscheinlich ist dies das erste, was mir in den Sinn kommt ... Sie passen jedoch erstens aufgrund ihrer typunsicheren Natur (wie viele Dinge von C) nicht. Zweitens müssen Sie bei der Verwendung solcher Argumente im Voraus wissen, um welche Arten von Argumenten es sich handelt. Mit ziemlicher Sicherheit sind dies jedoch nicht alle Probleme mit
Varargs . Im Allgemeinen ist dieses Tool hier kein Assistent.
Und jetzt werde ich die Tools auflisten, die mir bei der Lösung dieses Problems geholfen haben.
std :: any
Ab C ++ 17 hat die Sprache einen wunderbaren Container-Container für alles - eine entfernte Ähnlichkeit mit
System.Object in der CLI ist
std :: any . Dieser Behälter kann wirklich alles aufbewahren und sogar wie: effizient! - Der Standard empfiehlt, dass Sie kleine Objekte direkt darin speichern. Große Objekte können bereits im dynamischen Speicher gespeichert werden (obwohl dieses Verhalten nicht obligatorisch ist, hat Microsoft dies in seiner C ++ - Implementierung getan, was eine gute Nachricht ist). Und nur es kann als Ähnlichkeit bezeichnet werden, da System.Object an der Vererbungsbeziehung beteiligt ist ("is a") und std :: any an der Zugehörigkeitsbeziehung beteiligt ist ("has a"). Zusätzlich zu den Daten enthält der Container einen Zeiger auf ein Objekt
std :: type_info - RTTI über den Typ, dessen Objekt im Container "liegt".
Dem Container wird eine ganze Header-Datei
<any> zugewiesen.
Um ein Objekt aus dem Container zu ziehen, müssen Sie die Vorlagenfunktion
std :: any_cast () verwenden, die einen Verweis auf das Objekt zurückgibt.
Anwendungsbeispiel:
#include <any> void any_test() { std::any obj = 5; int from_any = std::any_cast<int>(obj); }
Wenn der angeforderte Typ nicht mit dem übereinstimmt, was das Objekt im Container hat, wird eine Ausnahme
std :: bad_any_cast ausgelöst .
Zusätzlich zu den
Klassen std :: any ,
std :: bad_any_cast und
std :: any_cast gibt es in der Header-Datei eine Vorlagenfunktion
std :: make_any , die
std :: make_shared ,
std :: make_pair und anderen Funktionen dieser Art
ähnelt .
RTTI
Natürlich wäre es in C ++ praktisch unrealistisch, einen dynamischen Funktionsaufruf ohne Typinformationen zur Laufzeit zu implementieren. Schließlich muss irgendwie überprüft werden, ob die richtigen Typen übergeben werden oder nicht.
Die primitive RTTI-Unterstützung in C ++ gibt es schon seit geraumer Zeit. Das ist nur der Punkt, der primitiv ist - wir können wenig über einen Typ lernen, es sei denn, dekorierte und nicht dekorierte Namen. Außerdem können wir Typen miteinander vergleichen.
Typischerweise wird der Begriff "RTTI" in Verbindung mit polymorphen Typen verwendet. Hier werden wir diesen Begriff jedoch im weiteren Sinne verwenden. Zum Beispiel werden wir die Tatsache berücksichtigen, dass jeder Typ zur Laufzeit Informationen über den Typ enthält (obwohl Sie diese im Gegensatz zu polymorphen Typen nur zur Kompilierungszeit statisch abrufen können). Daher ist es möglich (und notwendig), zur Laufzeit Typen auch nicht polymorpher Typen zu vergleichen (Entschuldigung für die Tautologie).
Auf RTTI kann mit der
Klasse std :: type_info zugegriffen werden. Diese Klasse befindet sich in der Header-Datei
<typeinfo> . Ein Verweis auf ein Objekt dieser Klasse kann (zumindest
vorerst ) nur mit dem Operator
typeid () abgerufen werden.
Muster
Ein weiteres äußerst wichtiges Merkmal der Sprache, das wir zur Verwirklichung unserer Ideen benötigen, sind Vorlagen. Dieses Tool ist sehr leistungsfähig und äußerst schwierig. Tatsächlich können Sie beim Kompilieren Code generieren.
Vorlagen sind ein sehr breites Thema, und es wird nicht möglich sein, sie im Rahmen des Artikels zu veröffentlichen, und es ist nicht erforderlich. Wir gehen davon aus, dass der Leser versteht, worum es geht. Dabei werden einige dunkle Punkte aufgedeckt.
Argumentumbruch gefolgt von einem Aufruf
Wir haben also eine bestimmte Funktion, die mehrere Parameter als Eingabe verwendet.
Ich zeige Ihnen eine Codeskizze, die meine Absichten erklärt.
#include <Variadic_args_binder.hpp> #include <string> #include <iostream> #include <vector> #include <any> int f(int a, std::string s) { std::cout << "int: " << a << "\nstring: " << s << std::endl; return 1; } void demo() { std::vector<std::any> params; params.push_back(5); params.push_back(std::string{ "Hello, Delegates!" }); delegates::Variadic_args_binder<int(*)(int, std::string), int, std::string> binder{ f, params }; binder(); }
Sie fragen sich vielleicht, wie ist das möglich? Der Klassenname
Variadic_args_binder gibt an, dass das Objekt die Funktion und die Argumente bindet, die Sie beim Aufrufen an das Objekt übergeben müssen. Es bleibt also nur, diesen Binder als Funktion ohne Parameter aufzurufen!
So sieht es draußen aus.
Wenn Sie sofort ohne nachzudenken davon ausgehen, wie dies implementiert werden kann, kann es in den Sinn kommen, mehrere
Variadic_args_binder- Spezialisierungen für eine andere Anzahl von Parametern zu schreiben. Dies ist jedoch nicht möglich, wenn eine unbegrenzte Anzahl von Parametern unterstützt werden muss. Und hier ist das Problem: Die Argumente müssen leider statisch in den Funktionsaufruf eingesetzt werden, dh letztendlich sollte für den Compiler der Aufrufcode darauf reduziert werden:
fun_ptr(param1, param2, …, paramN);
So funktioniert C ++. Und das alles erschwert das sehr.
Nur Template Magic kann damit umgehen!
Die Hauptidee besteht darin, rekursive Typen zu erstellen, die auf jeder Verschachtelungsebene eines der Argumente oder eine Funktion speichern.
Deklarieren Sie also die Klasse
_Tagged_args_binder :
namespace delegates::impl { template <typename Func_type, typename... T> class _Tagged_args_binder; }
Um Pakete vom Typ "bequem" zu "übertragen", erstellen wir einen
Hilfstyp ,
Type_pack_tag (warum dies benötigt wurde, wird bald klar):
template <typename... T> struct Type_pack_tag { };
Jetzt erstellen wir Spezialisierungen der Klasse
_Tagged_args_binder .
Erste Spezialisierungen
Wie Sie wissen, müssen Grenzfälle definiert werden, damit die Rekursion nicht unendlich ist.
Die folgenden Spezialisierungen sind initial. Der Einfachheit halber werde ich Spezialisierungen nur für Nichtreferenztypen und rWert-Referenztypen zitieren.
Spezialisierung für direkte Parameterwerte:
template <typename Func_type, typename T1, typename... Types_to_construct> class _Tagged_args_binder<Func_type, Type_pack_tag<T1, Types_to_construct...>, Type_pack_tag<>> { public: static_assert(!std::is_same_v<void, T1>, "Void argument is not allowed"); using Ret_type = std::invoke_result_t<Func_type, T1, Types_to_construct...>; _Tagged_args_binder(Func_type func, std::vector<std::any>& args) : ap_arg{ std::move(unihold::reference_any_cast<T1>(args.at(0))) }, ap_caller_part{ func, args } { } auto operator()() { if constexpr(std::is_same_v<void, Ret_type>) { ap_caller_part(std::move(ap_arg)); return; } else { return std::forward<Ret_type>(ap_caller_part(std::move(ap_arg))); } } auto operator()() const { if constexpr (std::is_same_v<void, Ret_type>) { ap_caller_part(std::move(ap_arg)); return; } else { return std::forward<Ret_type>(ap_caller_part(std::move(ap_arg))); } } private: _Tagged_args_binder<Func_type, Type_pack_tag<Types_to_construct...>, Type_pack_tag<T1>> ap_caller_part; T1 ap_arg; };
Das erste Argument für den Aufruf von
ap_arg und der Rest des rekursiven Objekts
ap_caller_part werden
hier gespeichert . Beachten Sie, dass der
T1- Typ vom ersten Typpaket in diesem Objekt zum zweiten im "Ende" des rekursiven Objekts "verschoben" wurde.
Spezialisierung für rvalue Links:
template <typename Func_type, typename T1, typename... Types_to_construct> class _Tagged_args_binder<Func_type, Type_pack_tag<T1&&, Types_to_construct...>, Type_pack_tag<>> { using move_ref_T1 = std::add_rvalue_reference_t<std::remove_reference_t<T1>>; public: using Ret_type = std::invoke_result_t<Func_type, move_ref_T1, Types_to_construct>; _Tagged_args_binder(Func_type func, std::vector<std::any>& args) : ap_arg{ std::move(unihold::reference_any_cast<T1>(args.at(0))) }, ap_caller_part{ func, args } { } auto operator()() { if constexpr (std::is_same_v<void, Ret_type>) { ap_caller_part(std::move(unihold::reference_any_cast<T1>(ap_arg))); } else { return std::forward<Ret_type>(ap_caller_part(std::move(unihold::reference_any_cast<T1>(ap_arg)))); } } auto operator()() const { if constexpr (std::is_same_v<void, Ret_type>) { ap_caller_part(std::move(unihold::reference_any_cast<T1>(ap_arg))); } else { return std::forward<Ret_type>(ap_caller_part(std::move(unihold::reference_any_cast<T1>(ap_arg)))); } } private: _Tagged_args_binder<Func_type, Type_pack_tag<Types_to_construct...>, Type_pack_tag<move_ref_T1>> ap_caller_part; std::any ap_arg; };
Vorlagen "rechtshändige" Links sind keine wirklich rechtshändigen Bedeutungen. Dies sind die sogenannten "Universal Links", die je nach Typ von
T1 entweder zu
T1 & oder zu
T1 && werden . Daher müssen Sie Problemumgehungen verwenden: Erstens wird beim Instanziieren der Vorlage die erforderliche Spezialisierung ausgewählt, auch wenn es sich um einen rechtshändigen Link handelt, da Spezialisierungen für beide Arten von Links definiert sind (dies wird aus den bereits genannten Gründen nicht ganz richtig gesagt) und für Nichtreferenzparameter, wenn Sie die Vorlage instanziieren. Zweitens wird zum Übertragen des
T1- Typs von Paket zu Paket die korrigierte Version von
move_ref_T1 verwendet , die in einen echten rvalue-Link umgewandelt wird.
Die Spezialisierung mit einem normalen Link erfolgt auf die gleiche Weise mit den erforderlichen Korrekturen.
Ultimative Spezialisierung
template <typename Func_type, typename... Param_type> class _Tagged_args_binder<Func_type, Type_pack_tag<>, Type_pack_tag<Param_type...>> { public: using Ret_type = std::invoke_result_t<Func_type, Param_type...>; inline _Tagged_args_binder(Func_type func, std::vector<std::any>& args) : ap_func{ func } { } inline auto operator()(Param_type... param) { if constexpr(std::is_same_v<void, decltype(ap_func(std::forward<Param_type>(param)...))>) { ap_func(std::forward<Param_type>(param)...); return; } else { return std::forward<Ret_type>(ap_func(std::forward<Param_type>(param)...)); } } inline auto operator()(Param_type... param) const { if constexpr(std::is_same_v<void, Ret_type>) { ap_func(param...); return; } else { return std::forward<Ret_type>(ap_func(param...)); } } private: Func_type ap_func; };
Diese Spezialisierung ist für die Speicherung eines Funktionsobjekts verantwortlich und ist in der Tat ein Wrapper darüber. Es ist der letzte rekursive Typ.
Beachten Sie, wie
Type_pack_tag hier verwendet wird. Alle Parametertypen werden jetzt im linken Paket kompiliert. Dies bedeutet, dass sie alle verarbeitet und verpackt werden.
Jetzt, denke ich, wird klar, warum es notwendig war,
Type_pack_tag zu verwenden. Tatsache ist, dass die Sprache die Verwendung von zwei Arten von Paketen nebeneinander nicht zulässt, zum Beispiel wie folgt:
template <typename Func_type, typename T1, typename... Types_to_construct, typename... Param_type> class _Tagged_args_binder<Func_type, T1, Types_to_construct..., Param_type...> { };
Daher müssen Sie sie in zwei separate Pakete innerhalb von zwei Typen aufteilen. Außerdem müssen Sie die verarbeiteten Typen irgendwie von denen trennen, die noch nicht verarbeitet wurden.
Fortgeschrittene Spezialisierungen
Von Zwischenspezialisierungen werde ich schließlich noch einmal eine Spezialisierung für Werttypen geben, der Rest ist analog:
template <typename Func_type, typename T1, typename... Types_to_construct, typename... Param_type> class _Tagged_args_binder<Func_type, Type_pack_tag<T1, Types_to_construct...>, Type_pack_tag<Param_type...>> { public: using Ret_type = std::invoke_result_t<Func_type, Param_type..., T1, Types_to_construct...>; static_assert(!std::is_same_v<void, T1>, "Void argument is not allowed"); inline _Tagged_args_binder(Func_type func, std::vector<std::any>& args) : ap_arg{ std::move(unihold::reference_any_cast<T1>(args.at(sizeof...(Param_type)))) }, ap_caller_part{ func, args } { } inline auto operator()(Param_type... param) { if constexpr (std::is_same_v<void, Ret_type>) { ap_caller_part(std::forward<Param_type>(param)..., std::move(ap_arg)); return; } else { return std::forward<Ret_type>(ap_caller_part(std::forward<Param_type>(param)..., std::move(ap_arg))); } } inline auto operator()(Param_type... param) const { if constexpr (std::is_same_v<void, Ret_type>) { ap_caller_part(std::forward<Param_type>(param)..., std::move(ap_arg)); } else { return std::forward<Ret_type>(ap_caller_part(std::forward<Param_type>(param)..., std::move(ap_arg))); } } private: _Tagged_args_binder<Func_type, Type_pack_tag<Types_to_construct...>, Type_pack_tag<Param_type..., T1>> ap_caller_part; T1 ap_arg; };
Diese Spezialisierung soll alle Argumente außer dem ersten packen.
Binder Klasse
Die Klasse
_Tagged_args_binder ist nicht für die direkte Verwendung vorgesehen, was ich am Anfang ihres Namens mit einem einzigen Unterstrich hervorheben wollte. Daher werde ich den Code einer kleinen Klasse angeben, die eine Art „Schnittstelle“ zu diesem hässlichen und unbequemen Typ darstellt (der jedoch meiner Meinung nach eher ungewöhnliche C ++ - Tricks verwendet, die ihm einen gewissen Charme verleihen):
namespace cutecpplib::delegates { template <typename Functor_type, typename... Param_type> class Variadic_args_binder { using binder_type = impl::_Tagged_args_binder<Functor_type, Type_pack_tag<Param_type...>, Type_pack_tag<>>; public: using Ret_type = std::invoke_result_t<binder_type>; inline Variadic_args_binder(Functor_type function, Param_type... param) : ap_tagged_binder{ function, param... } { } inline Variadic_args_binder(Functor_type function, std::vector<std::any>& args) : ap_tagged_binder{ function, args } { } inline auto operator()() { return ap_tagged_binder(); } inline auto operator()() const { return ap_tagged_binder(); } private: binder_type ap_tagged_binder; }; }
Unihold-Konvention - Weitergabe von Links innerhalb von std :: any
Ein aufmerksamer Leser muss bemerkt haben, dass der Code die Funktion
unihold :: reference_any_cast () verwendet. Diese Funktion sowie ihr analoges
unihold :: pointer_any_cast () dienen zur Implementierung der Bibliotheksvereinbarung: Die Argumente, die als Referenz übergeben werden müssen, werden per Zeiger an
std :: any übergeben .
Die Funktion
reference_any_cast gibt immer einen Verweis auf ein Objekt zurück, unabhängig davon, ob das Objekt selbst im Container gespeichert ist oder nur einen Zeiger darauf. Wenn
std :: any ein Objekt enthält, wird im Container ein Verweis auf dieses Objekt zurückgegeben. Wenn es einen Zeiger enthält, wird eine Referenz auf das Objekt zurückgegeben, auf das der Zeiger zeigt.
Für jede Funktion gibt es Optionen für die Konstante
std :: any und überladene Versionen, um zu bestimmen, ob der Container
std :: any ein Objekt besitzt oder nur einen Zeiger enthält.
Funktionen müssen explizit auf den Typ des gespeicherten Objekts spezialisiert sein, genau wie C ++ - Typkonvertierungen und ähnliche Vorlagenfunktionen.
Der Code für diese Funktionen:
template <typename T> std::remove_reference_t<T>& unihold::reference_any_cast(std::any& wrapper) { bool result; return reference_any_cast<T>(wrapper, result); } template <typename T> const std::remove_reference_t<T>& unihold::reference_any_cast(const std::any& wrapper) { bool result; return reference_any_cast<T>(wrapper, result); } template <typename T> std::remove_reference_t<T>& unihold::reference_any_cast(std::any& wrapper, bool& is_owner) { auto ptr = pointer_any_cast<T>(&wrapper, is_owner); if (!ptr) throw std::bad_any_cast{ }; return *ptr; } template <typename T> const std::remove_reference_t<T>& unihold::reference_any_cast(const std::any& wrapper, bool& is_owner) { auto ptr = pointer_any_cast<T>(&wrapper, is_owner); if (!ptr) throw std::bad_any_cast{ }; return *ptr; } template <typename T> std::remove_reference_t<T>* unihold::pointer_any_cast(std::any* wrapper, bool& is_owner) { using namespace std; using NR_T = remove_reference_t<T>;
Fazit
Ich habe versucht, einen der möglichen Ansätze zur Lösung des Problems dynamischer Funktionsaufrufe in C ++ kurz zu beschreiben. Anschließend bildet dies die Grundlage für die C ++ - Delegatenbibliothek (tatsächlich habe ich bereits die Grundfunktionalität der Bibliothek geschrieben, nämlich polymorphe Delegaten, aber die Bibliothek muss noch so umgeschrieben werden, wie sie sollte, um den Code zu demonstrieren und einige nicht realisierte Funktionen hinzuzufügen). In naher Zukunft plane ich, die Arbeit an der Bibliothek abzuschließen und zu erklären, wie genau ich den Rest der Delegatenfunktionalität in C ++ implementiert habe.
PS Die Verwendung von RTTI wird im nächsten Teil demonstriert.