Esta série de artigos será dedicada à possibilidade de criar um decorador em C ++, os recursos de seu trabalho em Python, e também consideraremos uma das opções para implementar essa funcionalidade em nossa própria linguagem compilada, usando a abordagem geral para criar closures - conversão de fechamento e modernização da árvore de sintaxe.

aviso Legal, Python — . Python , (). - ( ), - ( , ..), Python «» .
Decorador em C ++
Tudo começou com o fato de meu amigo VoidDruid ter decidido escrever um pequeno compilador como diploma, cuja principal característica são os decoradores. Mesmo durante a pré-defesa, quando ele destacou todas as vantagens de sua abordagem, que incluíam a alteração do AST, eu me perguntava: é realmente impossível implementar esses mesmos decoradores no grande e poderoso C ++ e fazer sem termos e abordagens complicados? Pesquisando neste tópico, não encontrei nenhuma abordagem simples e geral para resolver esse problema (a propósito, encontrei apenas artigos sobre a implementação do padrão de design) e depois me sentei para escrever meu próprio decorador.
No entanto, antes de passar para uma descrição direta da minha implementação, gostaria de falar um pouco sobre como as lambdas e os fechamentos em C ++ são organizados e qual é a diferença entre eles. Faça imediatamente uma reserva de que, se não houver menção a um padrão específico, por padrão, quero dizer C ++ 20. Em resumo, lambdas são funções anônimas e encerramentos são funções que usam objetos de seu ambiente. Por exemplo, começando com C ++ 11, um lambda pode ser declarado e chamado assim:
int main()
{
[] (int a)
{
std::cout << a << std::endl;
}(10);
}
Ou atribua seu valor a uma variável e chame-a mais tarde.int main()
{
auto lambda = [] (int a)
{
std::cout << a << std::endl;
};
lambda(10);
}
Mas o que acontece durante a compilação e o que é lambda? Para mergulhar na estrutura interna do lambda, basta ir ao site cppinsights.io e executar nosso primeiro exemplo. Em seguida, anexei uma conclusão possível:class __lambda_60_19
{
public:
inline void operator()(int a) const
{
std::cout.operator<<(a).operator<<(std::endl);
}
using retType_60_19 = void (*)(int);
inline operator retType_60_19 () const noexcept
{
return __invoke;
};
private:
static inline void __invoke(int a)
{
std::cout.operator<<(a).operator<<(std::endl);
}
};
Portanto, ao compilar, o lambda se transforma em uma classe, ou melhor, em um functor (um objeto para o qual o operador () está definido ) com um nome exclusivo gerado automaticamente que possui um operator () , que aceita os parâmetros que passamos para o lambda e seu corpo contém o código que nosso lambda deve executar. Com isso, tudo fica claro, mas e os outros dois métodos, por que eles são? O primeiro é o operador de conversão para um ponteiro de função, cujo protótipo coincide com nosso lambda, e o segundo é o código que deve ser executado quando nosso lambda é chamado em sua atribuição preliminar ao ponteiro, por exemplo,void (*p_lambda) (int) = lambda;
p_lambda(10);
Bem, há menos enigmas, mas e os fechamentos? Vamos escrever o exemplo mais simples de fechamento que captura a variável “a” por referência e a aumenta em um.int main()
{
int a = 10;
auto closure = [&a] () { a += 1; };
closure();
}
Como você pode ver, o mecanismo para criar closures e lambdas em C ++ é quase o mesmo; portanto, esses conceitos costumam ser confusos e lambdas e closures são simplesmente chamados de lambdas.Mas voltando à representação interna do fechamento em C ++.class __lambda_61_20
{
public:
inline void operator()()
{
a += 1;
}
private:
int & a;
public:
__lambda_61_20(int & _a)
: a{_a}
{}
};
Como você pode ver, adicionamos um novo construtor não padrão que pega nosso parâmetro por referência e o salva como membro da classe. Na verdade, é por isso que você precisa ser extremamente cuidadoso ao definir [&] ou [=], porque todo o contexto será armazenado dentro do fechamento e isso pode ser bastante subótimo da memória. Além disso, perdemos o operador de conversão para um ponteiro de função, porque agora é necessário o seu contexto de chamada normal. E agora o código acima não será compilado:
int main()
{
int a = 10;
auto closure = [&a] () { a += 1; };
closure();
void (*ptr)(int) = closure;
}
No entanto, se você ainda precisar passar o fechamento em algum lugar, ninguém cancelou o uso da função std ::.std::function<void()> function = closure;
function();
Agora que descobrimos aproximadamente o que são lambdas e fechamentos em C ++, passemos a escrever diretamente o decorador. Mas primeiro, você precisa decidir sobre nossos requisitos.Portanto, o decorador deve usar nossa função ou método como uma entrada, adicionar a funcionalidade necessária (por exemplo, isso será omitido) e retornar uma nova função quando for chamada, nosso código e o código da função / método forem executados. Neste ponto, qualquer pythonist que se preze diz: “Mas como assim! O decorador deve substituir o objeto original e qualquer chamada pelo nome deve chamar uma nova função! ” Apenas esta é a principal limitação do C ++, não podemos impedir que o usuário invoque a função antiga. Obviamente, existe uma opção para obter seu endereço na memória e triturá-lo (nesse caso, acessá-lo levará a um encerramento anormal do programa) ou substituir seu corpo por um aviso de que ele não deve ser usado no console, mas que está repleto de sérias conseqüências. Se a primeira opção parecer bastante difícil,o segundo, ao usar várias otimizações do compilador, também pode levar a uma falha e, portanto, não as usaremos. Além disso, o uso de qualquer macro mágica aqui considero redundante.Então, vamos escrever o nosso decorador. A primeira opção que me veio à mente foi esta:namespace Decorator
{
template<typename R, typename ...Args>
static auto make(const std::function<R(Args...)>& f)
{
std::cout << "Do something" << std::endl;
return [=](Args... args)
{
return f(std::forward<Args>(args)...);
};
}
};
Seja uma estrutura com um método estático que recebe std :: function e retorne um fechamento que terá os mesmos parâmetros que nossa função e, quando chamado, chamará nossa função e retornará seu resultado.Vamos criar uma função simples que queremos decorar.void myFunc(int a)
{
std::cout << "here" << std::endl;
}
E nosso principal ficará assim:int main()
{
std::function<void(int)> f = myFunc;
auto decorated = Decorator::make(f);
decorated(10);
}
Tudo funciona, está tudo bem e, em geral, Hurrah.Na verdade, esta solução tem vários problemas. Vamos começar em ordem:- Esse código só pode ser compilado com a versão C ++ 14 e superior, pois não é possível conhecer o tipo retornado antecipadamente. Infelizmente, tenho que conviver com isso e não encontrei outras opções.
- make exige que std :: function seja passado para ele, e a passagem de uma função pelo nome leva a erros de compilação. E isso não é tão conveniente quanto gostaríamos! Não podemos escrever código como este:
Decorator::make([](){});
Decorator::make(myFunc);
void(*ptr)(int) = myFunc;
Decorator::make(ptr);
- Além disso, não é possível decorar um método de classe.
Portanto, após uma breve conversa com colegas, a seguinte opção foi inventada para o C ++ 17 e superior:namespace Decorator
{
template<typename Function>
static auto make(Function&& func)
{
return [func = std::forward<Function>(func)] (auto && ...args)
{
std::cout << "Do something" << std::endl;
return std::invoke(
func,
std::forward<decltype(args)>(args)...
);
};
}
};
As vantagens dessa opção específica são que agora podemos decorar absolutamente qualquer objeto que tenha um operador () . Por exemplo, podemos passar o nome de uma função livre, um ponteiro, um lambda, qualquer functor, std :: function e, claro, um método de classe. No caso deste último, também será necessário passar um contexto para ele ao chamar a função decodificada.Opções de aplicaçãoint main()
{
auto decorated_1 = Decorator::make(myFunc);
decorated_1(1,2);
auto my_lambda = [] (int a, int b)
{
std::cout << a << " " << b <<std::endl;
};
auto decorated_2 = Decorator::make(my_lambda);
decorated_2(3,4);
int (*ptr)(int, int) = myFunc;
auto decorated_3 = Decorator::make(ptr);
decorated_3(5,6);
std::function<void(int, int)> fun = myFunc;
auto decorated_4 = Decorator::make(fun);
decorated_4(7,8);
auto decorated_5 = Decorator::make(decorated_4);
decorated_5(9, 10);
auto decorated_6 = Decorator::make(&MyClass::func);
decorated_6(MyClass(10));
}
Além disso, esse código pode ser compilado com o C ++ 14 se houver uma extensão para o uso de std :: invoke, que precisa ser substituída por std :: __ invoke. Se não houver extensão, você terá que desistir da capacidade de decorar métodos de classe, e essa funcionalidade ficará indisponível.Para não escrever o pesado "std :: forward <decltype (args)> (args) ...", você pode usar a funcionalidade disponível no C ++ 20 e fazer nosso lambda clichê!namespace Decorator
{
template<typename Function>
static auto make(Function&& func)
{
return [func = std::forward<Function>(func)]
<typename ...Args> (Args && ...args)
{
return std::invoke(
func,
std::forward<Args>(args)...
);
};
}
};
Tudo é perfeitamente seguro e até funciona da maneira que queremos (ou pelo menos fingimos). Esse código é compilado para as versões 10-x gcc e clang e você pode encontrá-lo aqui . Também haverá implementações para vários padrões.Nos próximos artigos, passaremos à implementação canônica de decoradores usando o exemplo Python e sua estrutura interna.