Aujourd'hui, j'ai un post assez court. Je ne l’écrirais probablement pas, mais sur Habré dans les commentaires, vous pouvez souvent trouver l’opinion que les pros empirent, le comité ne sait pas ce qui n’est pas clair pourquoi, et me rend généralement mon 2007e. Et puis un exemple aussi clair est soudainement apparu.
Il y a presque exactement cinq ans, j'ai écrit sur la façon de faire du curry en C ++. Eh bien, si vous pouviez écrire foo(bar, baz, quux)
, alors vous pourriez écrire Curry(foo)(bar)(baz)(quux)
. Ensuite, C ++ 14 vient de sortir et était à peine pris en charge par les compilateurs, donc le code n'utilisait que des puces C ++ 11 (plus quelques béquilles pour simuler les fonctions de bibliothèque à partir de C ++ 14).
Et puis je suis encore tombé sur ce code, et mes yeux me font mal à quel point il est verbeux. De plus, j'ai tourné le calendrier il n'y a pas si longtemps et j'ai remarqué que c'est maintenant l'année 2019, et vous pouvez voir comment C ++ 17 peut nous faciliter la vie.
Verrons-nous?
Ok, voyons.
L'implémentation originale, à partir de laquelle nous allons danser, ressemble à ceci:
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, {} }; }
Dans m_f
se trouve le foncteur stocké, dans m_prevArgs
- les arguments stockés lors des appels précédents.
Ici, operator()
doit déterminer s'il est déjà possible d'appeler le foncteur enregistré, ou s'il est nécessaire de continuer à accumuler des arguments, donc il fait un SFINAE assez standard en utilisant l'aide invoke
. De plus, afin d'appeler le foncteur (ou vérifier son appelabilité), nous couvrons le tout avec encore une autre couche SFINAE pour comprendre comment le faire (parce que nous devons appeler le pointeur sur le membre et, par exemple, la fonction libre de différentes manières), et pour cela nous utilisons la structure d'aide Invoke
, qui est probablement incomplète ... Bref, beaucoup de choses.
Eh bien, cette chose fonctionne de manière absolument dégoûtante avec la sémantique des mouvements, la transmission parfaite et d'autres mots doux au cœur du signe plus de notre temps. La réparation sera un peu plus difficile que nécessaire, car en plus de la tâche directement résolue, il existe également un tas de code qui ne lui est pas tout à fait lié.
Eh bien, encore une fois, en C ++ 11, il n'y a rien de tel que std::index_sequence
et des choses connexes, ou l'alias std::result_of_t
, donc le code C ++ 11 pur serait encore plus difficile.
Alors, enfin, passons au C ++ 17.
Tout d'abord, nous n'avons pas besoin de spécifier l' operator()
type de retour operator()
, nous pouvons écrire simplement:
template<typename T> auto operator() (const T& arg) const { return invoke (arg, 0); }
Techniquement, ce n'est pas exactement la même chose ("relier" est affiché de différentes manières), mais dans le cadre de notre tâche ce n'est pas essentiel.
De plus, nous n'avons pas besoin de faire SFINAE avec nos mains pour vérifier l' m_f
avec les arguments stockés. C ++ 17 nous offre deux fonctionnalités intéressantes: constexpr if
et std::is_invocable
. Jetez tout ce que nous avions avant et écrivez le squelette du nouvel operator()
:
template<typename T> auto operator() (const T& arg) const { if constexpr (std::is_invocable_v<F, PrevArgs..., T>)
La deuxième branche est triviale, vous pouvez copier le code qui était déjà:
template<typename T> auto operator() (const T& arg) const { if constexpr (std::is_invocable_v<F, PrevArgs..., T>)
La première branche sera plus intéressante. Nous devons appeler m_f
, en passant tous les arguments stockés dans m_prevArgs
, plus arg
. Heureusement, nous n'avons plus besoin de integer_sequence
: en C ++ 17, il existe une fonction de bibliothèque standard std::apply
pour appeler une fonction avec des arguments stockés dans tuple
. Seulement, nous devons mettre un autre argument ( arg
) à la fin du mannequin, afin que nous puissions soit créer std::tuple_cat
, soit simplement décompresser std::apply
', nous pouvons utiliser le lambda générique factice existant (une autre fonctionnalité apparue après C ++ 11, mais pas dans le 17!). D'après mon expérience, l'instanciation des mannequins est lente (en temps de calcul, bien sûr), je vais donc choisir la deuxième option. Dans le lambda lui-même, j'ai besoin d'appeler m_f
, et pour le faire correctement, je peux utiliser la fonction de bibliothèque qui est apparue en C ++ 17, std::invoke
, en lançant l'assistant Invoke
écrit à la main:
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 }) }; }
Il est utile de noter comment le type de retour déduit auto
vous permet de renvoyer des valeurs de différents types dans différentes branches if constexpr
.
En tout cas, c'est essentiellement tout. Ou avec le harnais nécessaire:
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)...) }; }
Je pense que c'est une amélioration significative par rapport à la version originale. Et c'est plus facile à lire. Même ennuyeux, pas de défi .
De plus, nous pourrions également nous débarrasser de la fonction Curry
et utiliser CurryImpl
directement, en nous appuyant sur des guides de déduction, mais cela est préférable lorsque nous traitons un transfert parfait, etc. Ce qui nous amène en douceur ...
Maintenant, il est assez évident à quel point c'est terrible en termes de copie d'arguments, de malheureuse transmission parfaite, etc. Mais plus important encore, la réparer est maintenant beaucoup plus facile. Mais nous le ferons en quelque sorte dans le prochain post.
Au lieu d'une conclusion
Premièrement, en C ++ 20, std::bind_front
apparaîtra, ce qui couvrira la part du lion de mes cas d'utilisateurs dans lesquels j'ai besoin d'une telle chose. Vous pouvez généralement le jeter. Triste
Deuxièmement, écrire sur les pros devient plus facile, même si vous écrivez une sorte de code de modèle avec une métaprogrammation. Vous n'avez plus à penser à l'option SFINAE à choisir, comment déballer le mannequin, comment appeler la fonction. Il suffit de prendre et d'écrire if constexpr
, std::apply
, std::invoke
. D'une part, c'est bien, je ne veux pas revenir au C ++ 14 ou, surtout, au 11. D'un autre côté, il semble que la couche de compétences d'un lion devienne inutile. Non, il est toujours utile de pouvoir visser quelque chose comme ça sur des modèles et de comprendre comment toute cette magie de bibliothèque fonctionne en vous, mais si vous en aviez besoin tout le temps, maintenant c'est beaucoup moins courant. Cela provoque des émotions étranges.