Sobre los nuevos estándares C ++

Hoy tengo una publicación bastante corta. Probablemente no lo escribiría, pero en Habré en los comentarios a menudo puedes encontrar la opinión de que los profesionales están empeorando, el comité no aclara por qué no está claro por qué, y generalmente me devuelve mi número 2007. Y entonces un ejemplo tan claro de repente apareció.


Hace casi exactamente cinco años, escribí sobre cómo hacer curry en C ++. Bueno, si pudieras escribir foo(bar, baz, quux) , entonces podrías escribir Curry(foo)(bar)(baz)(quux) . Luego, C ++ 14 acaba de salir y apenas era compatible con los compiladores, por lo que el código solo usaba chips C ++ 11 (más un par de muletas para simular funciones de biblioteca de C ++ 14).


Y luego me topé con este código nuevamente, y mis ojos me dolieron por lo detallado que es. Además, cambié el calendario hace poco y noté que ahora es el año 2019, y puedes ver cómo C ++ 17 puede hacernos la vida más fácil.


Vamos a ver?


Ok, veamos


La implementación original, de la que bailaremos, se ve así:


 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, {} }; } 

En m_f encuentra el functor almacenado, en m_prevArgs , los argumentos almacenados en llamadas anteriores.


Aquí operator() debe determinar si ya es posible llamar al functor guardado, o si es necesario continuar acumulando argumentos, por lo que hace un SFINAE bastante estándar utilizando el asistente de invoke . Además, para llamar al functor (o verificar su capacidad de recuperación), cubrimos todo con otra capa SFINAE para entender cómo hacerlo (porque necesitamos llamar al puntero al miembro y, por ejemplo, la función libre de diferentes maneras), y para esto usamos la estructura auxiliar Invoke , que probablemente esté incompleta ... En resumen, muchas cosas.


Bueno, esto funciona de manera absolutamente repugnante con semántica de movimiento, reenvío perfecto y otras palabras dulces para el corazón del signo más de nuestro tiempo. Reparar esto será un poco más difícil de lo necesario, ya que además de la tarea resuelta directamente, también hay un montón de código que no está muy relacionado con él.


Bueno, de nuevo, en C ++ 11 no hay cosas como std::index_sequence y cosas relacionadas, o el alias std::result_of_t , por lo que el código puro de C ++ 11 sería aún más difícil.


Entonces, finalmente, pasemos a C ++ 17.


En primer lugar, no necesitamos especificar el operator() tipo de retorno operator() , podemos escribir simplemente:


 template<typename T> auto operator() (const T& arg) const { return invoke (arg, 0); } 

Técnicamente, esto no es exactamente lo mismo ("vinculación" se muestra de diferentes maneras), pero en el marco de nuestra tarea esto no es esencial.


Además, no necesitamos hacer SFINAE con nuestras manos para verificar la m_f con los argumentos almacenados. C ++ 17 nos ofrece dos características interesantes: constexpr if y std::is_invocable . Deseche todo lo que teníamos antes y escriba el esqueleto del nuevo operator() :


 template<typename T> auto operator() (const T& arg) const { if constexpr (std::is_invocable_v<F, PrevArgs..., T>) //   else //       arg } 

La segunda rama es trivial, puede copiar el código que ya estaba:


 template<typename T> auto operator() (const T& arg) const { if constexpr (std::is_invocable_v<F, PrevArgs..., T>) //   else return CurryImpl<F, PrevArgs..., T> { m_f, std::tuple_cat (m_prevArgs, std::tuple<T> { arg }) }; } 

La primera rama será más interesante. Necesitamos llamar a m_f , pasando todos los argumentos almacenados en m_prevArgs , más arg . Afortunadamente, ya no necesitamos ninguna integer_sequence : en C ++ 17 hay una función de biblioteca estándar std::apply para llamar a una función con argumentos almacenados en tuple . Solo necesitamos poner otro argumento ( arg ) al final del dummy, para que podamos hacer std::tuple_cat , o simplemente descomprimir std::apply ', podemos usar el lambda genérico ficticio existente (otra característica que apareció después de C ++ 11, ¡aunque no en el 17!). En mi experiencia, la creación de dummies es lenta (en tiempo de cálculo, por supuesto), por lo que elegiré la segunda opción. En el lambda en sí, necesito llamar a m_f , y para hacer esto correctamente, puedo usar la función de biblioteca que apareció en C ++ 17, std::invoke , tirando el asistente de Invoke escrito a mano:


 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 útil observar cómo el tipo de retorno deducido auto permite devolver valores de diferentes tipos en diferentes ramas if constexpr .


En cualquier caso, eso es básicamente todo. O junto con el arnés necesario:


 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)...) }; } 

Creo que esta es una mejora significativa sobre la versión original. Y es más fácil de leer. Incluso de alguna manera aburrida, no hay desafío .


Además, también podríamos deshacernos de la función Curry y usar CurryImpl directamente, confiando en las guías de deducción, pero esto se hace mejor cuando tratamos con el reenvío perfecto y similares. Lo que sin problemas nos trae ...


Ahora es bastante obvio lo terrible que es esto en términos de copiar argumentos, este desafortunado reenvío perfecto y demás. Pero lo más importante, arreglarlo ahora es mucho más fácil. Pero, sin embargo, haremos esto de alguna manera en la próxima publicación.


En lugar de una conclusión


En primer lugar, en C ++ 20, aparecerá std::bind_front , que cubrirá la mayor parte de mis casos de usuario en los que necesito tal cosa. Generalmente puedes tirarlo a la basura. Triste


En segundo lugar, escribir sobre los profesionales es cada vez más fácil, incluso si escribe algún tipo de código de plantilla con metaprogramación. Ya no tiene que pensar qué opción de SFINAE elegir, cómo desempaquetar el dummy, cómo llamar a la función. Simplemente tome y escriba if constexpr , std::apply , std::invoke . Por un lado, es bueno; no quiero volver a C ++ 14 o, especialmente, 11. Por otro lado, parece que la capa de habilidades de un león se está volviendo innecesaria. No, todavía es útil poder atornillar algo así en las plantillas y comprender cómo funciona toda esta magia de la biblioteca dentro de ti, pero si solías necesitarla todo el tiempo, ahora es mucho menos común. Causa algunas emociones extrañas.

Source: https://habr.com/ru/post/466985/


All Articles