Sobre os novos padrões C ++

Hoje eu tenho um post bem curto. Provavelmente, eu não escreveria, mas em Habré, nos comentários, é comum encontrar a opinião de que os profissionais estão piorando, o comitê deixa claro o que não está claro por que e geralmente me devolve o meu ano 2007. E então um exemplo tão claro apareceu de repente.


Quase exatamente cinco anos atrás, escrevi sobre como fazer curry em C ++. Bem, se você pudesse escrever foo(bar, baz, quux) , então poderia escrever Curry(foo)(bar)(baz)(quux) . Então o C ++ 14 acabou de ser lançado e era pouco suportado pelos compiladores; portanto, o código usava apenas os chips C ++ 11 (mais algumas muletas para simular as funções de biblioteca do C ++ 14).


E então me deparei com esse código novamente, e meus olhos doem com o quão detalhado é. Além disso, virei o calendário há pouco tempo e notei que agora é o ano de 2019, e você pode ver como o C ++ 17 pode facilitar nossa vida.


Vamos ver?


Ok, vamos ver.


A implementação original, da qual iremos dançar, é mais ou menos assim:


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

Em m_f está o functor armazenado, em m_prevArgs - os argumentos armazenados em chamadas anteriores.


Aqui, o operator() deve determinar se já é possível chamar o functor salvo ou se é necessário continuar a acumular argumentos, para criar um SFINAE bastante padrão usando o auxiliar de chamada. Além disso, para chamar o functor (ou verificar sua capacidade de chamar), cobrimos tudo com mais uma camada SFINAE para entender como fazê-lo (porque precisamos chamar o ponteiro para o membro e, por exemplo, a função livre de diferentes maneiras), e, para isso, usamos a estrutura auxiliar Invoke , que provavelmente está incompleta ... Em suma, muitas coisas.


Bem, essa coisa absolutamente nojenta funciona com semântica de movimento, encaminhamento perfeito e outras palavras doces para o coração do sinal de mais do nosso tempo. Reparar isso será um pouco mais difícil do que o necessário, pois além da tarefa diretamente resolvida, há também um monte de código que não está relacionado a ela.


Bem, novamente, no C ++ 11 não existem coisas como std::index_sequence e coisas relacionadas, ou o alias std::result_of_t , portanto, o código C ++ 11 puro seria ainda mais difícil.


Então, finalmente, vamos para o C ++ 17.


Primeiramente, não precisamos especificar o tipo de retorno operator() , podemos escrever simplesmente:


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

Tecnicamente, isso não é exatamente o mesmo ("ligação" é exibida de maneiras diferentes), mas na estrutura de nossa tarefa isso não é essencial.


Além disso, não precisamos executar o SFINAE com nossas mãos para verificar a m_f com os argumentos armazenados. O C ++ 17 nos fornece dois recursos interessantes: constexpr if e std::is_invocable . Jogue fora tudo o que tínhamos antes e escreva o esqueleto do novo operator() :


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

O segundo ramo é trivial, você pode copiar o código que já era:


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

O primeiro ramo será mais interessante. Precisamos chamar m_f , passando todos os argumentos armazenados em m_prevArgs , mais arg . Felizmente, não precisamos mais de integer_sequence : no C ++ 17, existe uma função de biblioteca padrão std::apply para chamar uma função com argumentos armazenados na tuple . Somente precisamos colocar outro argumento ( arg ) no final do manequim, para que possamos criar std::tuple_cat ou apenas descompactar std::apply ', podemos usar o lambda genérico fictício existente (outro recurso que apareceu após o C ++ 11, embora não no dia 17!). Na minha experiência, instanciar manequins é lento (em tempo de computação, é claro), então escolherei a segunda opção. No próprio lambda, preciso chamar m_f e, para fazer isso corretamente, posso usar a função de biblioteca que apareceu no C ++ 17, std::invoke , jogando fora o auxiliar Invoke escrito à mão:


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

É útil observar como o tipo de retorno deduzido auto permite retornar valores de tipos diferentes em diferentes ramificações, if constexpr .


De qualquer forma, isso é basicamente tudo. Ou junto com o chicote necessário:


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

Eu acho que isso é uma melhoria significativa em relação à versão original. E é mais fácil de ler. Mesmo de alguma forma chata, sem desafio .


Além disso, também podemos nos livrar da função Curry e usar o CurryImpl diretamente, contando com guias de dedução, mas isso é melhor quando lidamos com encaminhamento perfeito e coisas do gênero. O que nos traz suavemente ...


Agora, é óbvio o quão terrível isso é em termos de argumentos de cópia, esse encaminhamento perfeito e lamentável e assim por diante. Mais importante, porém, corrigi-lo agora é muito mais fácil. Mas nós, no entanto, faremos isso de alguma forma no próximo post.


Em vez de uma conclusão


Primeiramente, no C ++ 20, std::bind_front aparecerá, o que cobrirá a maior parte dos casos de usuários nos quais eu preciso disso. Geralmente você pode jogá-lo fora. Triste


Em segundo lugar, escrever sobre os profissionais está ficando mais fácil, mesmo se você escrever algum tipo de código de modelo com metaprogramação. Você não precisa mais pensar em qual opção SFINAE escolher, como descompactar o manequim, como chamar a função. Basta pegar e escrever if constexpr , std::apply , std::invoke . Por um lado, é bom; não quero voltar ao C ++ 14 ou, principalmente, 11. Por outro lado, parece que a camada de habilidades de um leão está se tornando desnecessária. Não, ainda é útil poder estragar algo assim nos modelos e entender como toda essa mágica da biblioteca funciona dentro de você, mas se você costumava precisar disso o tempo todo, agora é muito menos comum. Isso causa algumas emoções estranhas.

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


All Articles