Heute habe ich einen ziemlich kurzen Beitrag. Ich würde es wahrscheinlich nicht schreiben, aber auf Habré in den Kommentaren kann man oft die Meinung finden, dass die Profis schlechter werden. Das Komitee macht unklar, was unklar ist, warum und gibt mir im Allgemeinen mein 2007 zurück. Und dann kam plötzlich ein so klares Beispiel auf.
Vor fast genau fünf Jahren schrieb ich darüber, wie man Curry in C ++ macht. Wenn Sie foo(bar, baz, quux)
schreiben könnten, dann könnten Sie Curry(foo)(bar)(baz)(quux)
schreiben. Dann kam C ++ 14 heraus und wurde von Compilern kaum unterstützt, sodass der Code nur C ++ 11-Chips verwendete (plus ein paar Krücken, um Bibliotheksfunktionen aus C ++ 14 zu simulieren).
Und dann bin ich wieder auf diesen Code gestoßen, und meine Augen tun weh, wie ausführlich er ist. Außerdem habe ich vor nicht allzu langer Zeit den Kalender umgedreht und festgestellt, dass jetzt das Jahr 2019 ist, und Sie können sehen, wie C ++ 17 unser Leben einfacher machen kann.
Sollen wir sehen?
Ok, mal sehen.
Die ursprüngliche Implementierung, aus der wir tanzen werden, sieht ungefähr so aus:
template<typename F, typename... PrevArgs> class CurryImpl { const F m_f; const std::tuple<PrevArgs...> m_prevArgs; public: CurryImpl (F f, const std::tuple<PrevArgs...>& prev) : m_f { f } , m_prevArgs { prev } { } private: template<typename T> std::result_of_t<F (PrevArgs..., T)> invoke (const T& arg, int) const { return invokeIndexed (arg, std::index_sequence_for<PrevArgs...> {}); } template<typename IF> struct Invoke { template<typename... IArgs> auto operator() (IF fr, IArgs... args) -> decltype (fr (args...)) { return fr (args...); } }; template<typename R, typename C, typename... Args> struct Invoke<R (C::*) (Args...)> { R operator() (R (C::*ptr) (Args...), C c, Args... rest) { return (c.*ptr) (rest...); } R operator() (R (C::*ptr) (Args...), C *c, Args... rest) { return (c->*ptr) (rest...); } }; template<typename T, std::size_t... Is> auto invokeIndexed (const T& arg, std::index_sequence<Is...>) const -> decltype (Invoke<F> {} (m_f, std::get<Is> (m_prevArgs)..., arg)) { return Invoke<F> {} (m_f, std::get<Is> (m_prevArgs)..., arg); } template<typename T> auto invoke (const T& arg, ...) const -> CurryImpl<F, PrevArgs..., T> { return { m_f, std::tuple_cat (m_prevArgs, std::tuple<T> { arg }) }; } public: template<typename T> auto operator() (const T& arg) const -> decltype (invoke (arg, 0)) { return invoke (arg, 0); } }; template<typename F> CurryImpl<F> Curry (F f) { return { f, {} }; }
In m_f
liegt der gespeicherte Funktor, in m_prevArgs
die Argumente, die bei früheren Aufrufen gespeichert wurden.
Hier muss operator()
bestimmen, ob es bereits möglich ist, den gespeicherten Funktor aufzurufen, oder ob es notwendig ist, weiterhin Argumente zu akkumulieren, sodass mit dem Aufrufhelfer eine ziemlich standardmäßige SFINAE erstellt wird. Um den Funktor aufzurufen (oder seine Aufrufbarkeit zu überprüfen), decken wir alles mit einer weiteren SFINAE-Ebene ab, um zu verstehen, wie es geht (weil wir den Zeiger auf das Mitglied und beispielsweise die freie Funktion auf verschiedene Arten aufrufen müssen). und dafür verwenden wir die Invoke
Hilfsstruktur, die wahrscheinlich unvollständig ist ... Kurz gesagt, viele Dinge.
Nun, dieses Ding funktioniert absolut widerlich mit Bewegungssemantik, perfekter Weiterleitung und anderen Worten, die dem Pluszeichen unserer Zeit am Herzen liegen. Das Reparieren ist etwas schwieriger als nötig, da es neben der direkt gelösten Aufgabe auch eine Reihe von Code gibt, die nicht ganz damit zusammenhängen.
Nun, in C ++ 11 gibt es keine Dinge wie std::index_sequence
und verwandte Dinge oder den Alias std::result_of_t
, so dass reiner C ++ 11-Code noch schwieriger wäre.
Kommen wir zum Schluss zu C ++ 17.
Erstens müssen wir den operator()
für den Rückgabetyp operator()
nicht angeben, wir können einfach schreiben:
template<typename T> auto operator() (const T& arg) const { return invoke (arg, 0); }
Technisch ist dies nicht genau das gleiche ("Verknüpfen" wird auf unterschiedliche Weise angezeigt), aber im Rahmen unserer Aufgabe ist dies nicht wesentlich.
Außerdem müssen wir SFINAE nicht mit unseren Händen m_f
, um die m_f
mit den gespeicherten Argumenten zu überprüfen. C ++ 17 bietet zwei coole Funktionen: constexpr if
und std::is_invocable
. Wirf alles raus, was wir vorher hatten und schreibe das Skelett des neuen operator()
:
template<typename T> auto operator() (const T& arg) const { if constexpr (std::is_invocable_v<F, PrevArgs..., T>)
Der zweite Zweig ist trivial, Sie können den Code kopieren, der bereits war:
template<typename T> auto operator() (const T& arg) const { if constexpr (std::is_invocable_v<F, PrevArgs..., T>)
Der erste Zweig wird interessanter sein. Wir müssen m_f
aufrufen und alle in m_prevArgs
gespeicherten Argumente plus arg
. Zum Glück brauchen wir keine integer_sequence
: In C ++ 17 gibt es eine Standardbibliotheksfunktion std::apply
, um eine Funktion mit in tuple
gespeicherten Argumenten aufzurufen. Nur müssen wir ein weiteres Argument ( arg
) am Ende des Dummys std::tuple_cat
, damit wir entweder std::tuple_cat
oder einfach std::apply
entpacken können. Wir können das vorhandene generische Dummy-Lambda verwenden (eine weitere Funktion, die nach C ++ std::tuple_cat
wurde 11, wenn auch nicht im 17.!). Nach meiner Erfahrung ist das Instanziieren von Dummies langsam (natürlich in der Rechenzeit), daher wähle ich die zweite Option. Im Lambda selbst muss ich m_f
aufrufen. m_f
dies korrekt m_f
, kann ich die in C ++ 17, std::invoke
m_f
, m_f
Bibliotheksfunktion verwenden, indem ich den von Hand geschriebenen Invoke
Helfer Invoke
:
template<typename T> auto operator() (const T& arg) const { if constexpr (std::is_invocable_v<F, PrevArgs..., T>) { auto wrapper = [this, &arg] (auto&&... args) { return std::invoke (m_f, std::forward<decltype (args)> (args)..., arg); }; return std::apply (std::move (wrapper), m_prevArgs); } else return CurryImpl<F, PrevArgs..., T> { m_f, std::tuple_cat (m_prevArgs, std::tuple<T> { arg }) }; }
Es ist nützlich zu beachten, wie Sie mit dem auto
abgeleiteten Rückgabetyp Werte verschiedener Typen in verschiedenen Zweigen zurückgeben können, if constexpr
.
In jedem Fall ist das im Grunde alles. Oder zusammen mit dem notwendigen Gurt:
template<typename F, typename... PrevArgs> class CurryImpl { const F m_f; const std::tuple<PrevArgs...> m_prevArgs; public: CurryImpl (F f, const std::tuple<PrevArgs...>& prev) : m_f { f } , m_prevArgs { prev } { } template<typename T> auto operator() (const T& arg) const { if constexpr (std::is_invocable_v<F, PrevArgs..., T>) { auto wrapper = [this, &arg] (auto&&... args) { return std::invoke (m_f, std::forward<decltype (args)> (args)..., arg); }; return std::apply (std::move (wrapper), m_prevArgs); } else return CurryImpl<F, PrevArgs..., T> { m_f, std::tuple_cat (m_prevArgs, std::tuple<T> { arg }) }; } }; template<typename F, typename... Args> CurryImpl<F, Args...> Curry (F f, Args&&... args) { return { f, std::forward_as_tuple (std::forward<Args> (args)...) }; }
Ich denke, dies ist eine signifikante Verbesserung gegenüber der Originalversion. Und es ist einfacher zu lesen. Auch irgendwie langweilig, keine Herausforderung .
Darüber hinaus könnten wir auch die Curry
Funktion loswerden und CurryImpl
direkt verwenden, wobei wir uns auf CurryImpl
Dies ist jedoch am besten möglich, wenn wir uns mit perfekter Weiterleitung und dergleichen befassen. Was uns reibungslos bringt ...
Jetzt ist es ziemlich offensichtlich, wie schrecklich dies in Bezug auf das Kopieren von Argumenten, diese unglückliche perfekte Weiterleitung und dergleichen ist. Noch wichtiger ist jedoch, dass das Reparieren jetzt viel einfacher ist. Aber wir werden dies im nächsten Beitrag irgendwie tun.
Anstelle einer Schlussfolgerung
Zunächst wird in C ++ 20 std::bind_front
, das den Löwenanteil meiner Benutzerfälle std::bind_front
, in denen ich so etwas benötige. Sie können es im Allgemeinen wegwerfen. Traurig
Zweitens wird das Schreiben auf den Profis einfacher, selbst wenn Sie eine Art Vorlagencode mit Metaprogrammierung schreiben. Sie müssen nicht mehr darüber nachdenken, welche SFINAE-Option Sie auswählen, wie Sie den Dummy entpacken und wie Sie die Funktion aufrufen. Nehmen Sie einfach und schreiben Sie, if constexpr
, std::apply
, std::invoke
. Einerseits ist es gut, ich möchte nicht zu C ++ 14 oder insbesondere 11 zurückkehren. Auf der anderen Seite scheint es, als würde die Fähigkeitsschicht eines Löwen unnötig. Nein, es ist immer noch nützlich, in der Lage zu sein, so etwas auf Vorlagen zu schrauben und zu verstehen, wie all diese Bibliothekszauber in Ihnen funktionieren, aber wenn Sie sie früher immer gebraucht haben, ist sie jetzt viel seltener. Es verursacht einige seltsame Emotionen.