Antecedentes
Eu gosto da linguagem C ++. Eu diria até que este é o meu idioma favorito. Além disso, uso tecnologias .NET para o meu desenvolvimento, e muitas das idéias contidas nele, na minha opinião, são simplesmente incríveis. Depois que surgiu a idéia - como implementar alguns meios de reflexão e chamadas de funções dinâmicas em C ++? Eu realmente queria que o C ++ tivesse uma vantagem de CLI, como chamar um delegado com um número desconhecido de parâmetros e seus tipos. Isso pode ser útil, por exemplo, quando não se sabe com antecedência quais tipos de dados a função precisa ser chamada.
Obviamente, uma imitação completa de delegados é muito complicada; portanto, este artigo demonstrará apenas a arquitetura geral da biblioteca e a solução para alguns problemas importantes que surgem ao lidar com o que não é diretamente suportado pelo idioma.
Chamando funções com um número indefinido de parâmetros e tipos desconhecidos durante a compilação
Obviamente, esse é o principal problema do C ++, que não é tão fácil de resolver. Obviamente, em C ++ há uma ferramenta herdada de C-
varargs , e provavelmente essa é a primeira coisa que vem à mente ... No entanto, elas não se encaixam, primeiramente, por causa de sua natureza insegura (como muitas coisas de C), segundo, ao usar esses argumentos, você precisa saber com antecedência quais são os tipos de argumentos. No entanto, quase certamente, este não é todos os problemas com
varargs . Em geral, esta ferramenta não é um assistente aqui.
E agora vou listar as ferramentas que me ajudaram a resolver esse problema.
std :: any
A partir do C ++ 17, a linguagem possui um contêiner de contêiner maravilhoso para qualquer coisa - alguma semelhança distante com
System.Object na CLI é
std :: any . Este contêiner pode realmente armazenar qualquer coisa, e até como: eficientemente! - o padrão recomenda que você armazene objetos pequenos diretamente, objetos grandes já podem ser armazenados na memória dinâmica (embora esse comportamento não seja obrigatório, a Microsoft fez isso na implementação do C ++, o que é uma boa notícia). E somente isso pode ser chamado de similaridade porque System.Object está envolvido no relacionamento de herança ("é um") e std :: any está envolvido no relacionamento de associação ("tem um"). Além dos dados, o contêiner contém um ponteiro para o objeto
std :: type_info - RTTI sobre o tipo cujo objeto "está" no contêiner.
Um arquivo de cabeçalho inteiro
<qualquer> é alocado para o contêiner.
Para "puxar" um objeto do contêiner, você precisa usar a função de modelo
std :: any_cast () , que retorna uma referência ao objeto.
Exemplo de uso:
#include <any> void any_test() { std::any obj = 5; int from_any = std::any_cast<int>(obj); }
Se o tipo solicitado não corresponder ao que o objeto possui dentro do contêiner, uma exceção
std :: bad_any_cast será lançada .
Além das
classes std :: any ,
std :: bad_any_cast e das
funções std :: any_cast , no arquivo de cabeçalho, existe uma função de modelo
std :: make_any semelhante a
std :: make_shared ,
std :: make_pair e outras funções desse tipo.
RTTI
Obviamente, seria praticamente irreal no C ++ implementar uma chamada de função dinâmica sem informações de tipo em tempo de execução. Afinal, é necessário verificar se os tipos corretos são aprovados ou não.
O suporte a RTTI primitivo em C ++ já existe há algum tempo. Esse é apenas o ponto, que é primitivo - podemos aprender pouco sobre um tipo, a menos que nomes decorados e não decorados. Além disso, podemos comparar tipos entre si.
Normalmente, o termo "RTTI" é usado em conexão com tipos polimórficos. No entanto, aqui usaremos esse termo em um sentido mais amplo. Por exemplo, levaremos em conta o fato de que cada tipo possui informações sobre o tipo em tempo de execução (embora você possa obtê-las estaticamente apenas em tempo de compilação, diferentemente dos tipos polimórficos). Portanto, é possível (e necessário) comparar tipos de tipos não polimórficos (desculpe a tautologia) em tempo de execução.
O RTTI pode ser acessado usando a
classe std :: type_info . Essa classe está localizada no arquivo de cabeçalho
<typeinfo> . Uma referência a um objeto desta classe pode ser obtida (pelo menos por enquanto) apenas usando o operador
typeid () .
Padrões
Outra característica extremamente importante da linguagem que precisamos para realizar nossas idéias são os modelos. Essa ferramenta é bastante poderosa e extremamente difícil, na verdade, permite gerar código em tempo de compilação.
Os modelos são um tópico muito amplo, e não será possível revelá-lo na estrutura do artigo, e não é necessário. Assumimos que o leitor entenda do que se trata. Alguns pontos obscuros serão revelados no processo.
Quebra de argumento seguida por uma chamada
Portanto, temos uma certa função que recebe vários parâmetros como entrada.
Vou mostrar um esboço de código que explica minhas intenções.
#include <Variadic_args_binder.hpp> #include <string> #include <iostream> #include <vector> #include <any> int f(int a, std::string s) { std::cout << "int: " << a << "\nstring: " << s << std::endl; return 1; } void demo() { std::vector<std::any> params; params.push_back(5); params.push_back(std::string{ "Hello, Delegates!" }); delegates::Variadic_args_binder<int(*)(int, std::string), int, std::string> binder{ f, params }; binder(); }
Você pode perguntar, como isso é possível? O nome da classe
Variadic_args_binder informa que o objeto liga a função e os argumentos que você precisa passar para ela quando a chama. Portanto, resta apenas chamar esse fichário como uma função sem parâmetros!
Então parece lá fora.
Se imediatamente, sem pensar, faça uma suposição de como isso pode ser implementado, pode ser necessário escrever várias especializações
Variadic_args_binder para um número diferente de parâmetros. No entanto, isso não é possível se for necessário suportar um número ilimitado de parâmetros. E aqui está o problema: os argumentos, infelizmente, precisam ser substituídos estaticamente na chamada de função, ou seja, em última análise, para o compilador, o código de chamada deve ser reduzido a isso:
fun_ptr(param1, param2, …, paramN);
É assim que o C ++ funciona. E tudo isso complica muito.
Somente a mágica do modelo pode lidar com isso!
A idéia principal é criar tipos recursivos que armazenam em cada nível de aninhamento um dos argumentos ou uma função.
Portanto, declare a classe
_Tagged_args_binder :
namespace delegates::impl { template <typename Func_type, typename... T> class _Tagged_args_binder; }
Para "transferir" convenientemente pacotes do tipo, criaremos um tipo auxiliar,
Type_pack_tag (por que isso foi necessário, ficará claro em breve):
template <typename... T> struct Type_pack_tag { };
Agora criamos especializações da classe
_Tagged_args_binder .
Especializações Iniciais
Como você sabe, para que a recursão não seja infinita, é necessário definir casos de fronteira.
As seguintes especializações são iniciais. Para simplificar, citarei especializações apenas para tipos de não referência e tipos de referência rvalue.
Especialização para valores de parâmetros diretamente:
template <typename Func_type, typename T1, typename... Types_to_construct> class _Tagged_args_binder<Func_type, Type_pack_tag<T1, Types_to_construct...>, Type_pack_tag<>> { public: static_assert(!std::is_same_v<void, T1>, "Void argument is not allowed"); using Ret_type = std::invoke_result_t<Func_type, T1, Types_to_construct...>; _Tagged_args_binder(Func_type func, std::vector<std::any>& args) : ap_arg{ std::move(unihold::reference_any_cast<T1>(args.at(0))) }, ap_caller_part{ func, args } { } auto operator()() { if constexpr(std::is_same_v<void, Ret_type>) { ap_caller_part(std::move(ap_arg)); return; } else { return std::forward<Ret_type>(ap_caller_part(std::move(ap_arg))); } } auto operator()() const { if constexpr (std::is_same_v<void, Ret_type>) { ap_caller_part(std::move(ap_arg)); return; } else { return std::forward<Ret_type>(ap_caller_part(std::move(ap_arg))); } } private: _Tagged_args_binder<Func_type, Type_pack_tag<Types_to_construct...>, Type_pack_tag<T1>> ap_caller_part; T1 ap_arg; };
O primeiro argumento para a chamada
ap_arg e o restante do objeto
ap_caller_part recursivo são
armazenados aqui . Observe que o tipo
T1 "mudou" do primeiro pacote de tipos nesse objeto para o segundo na "cauda" do objeto recursivo.
Especialização para links rvalue:
template <typename Func_type, typename T1, typename... Types_to_construct> class _Tagged_args_binder<Func_type, Type_pack_tag<T1&&, Types_to_construct...>, Type_pack_tag<>> { using move_ref_T1 = std::add_rvalue_reference_t<std::remove_reference_t<T1>>; public: using Ret_type = std::invoke_result_t<Func_type, move_ref_T1, Types_to_construct>; _Tagged_args_binder(Func_type func, std::vector<std::any>& args) : ap_arg{ std::move(unihold::reference_any_cast<T1>(args.at(0))) }, ap_caller_part{ func, args } { } auto operator()() { if constexpr (std::is_same_v<void, Ret_type>) { ap_caller_part(std::move(unihold::reference_any_cast<T1>(ap_arg))); } else { return std::forward<Ret_type>(ap_caller_part(std::move(unihold::reference_any_cast<T1>(ap_arg)))); } } auto operator()() const { if constexpr (std::is_same_v<void, Ret_type>) { ap_caller_part(std::move(unihold::reference_any_cast<T1>(ap_arg))); } else { return std::forward<Ret_type>(ap_caller_part(std::move(unihold::reference_any_cast<T1>(ap_arg)))); } } private: _Tagged_args_binder<Func_type, Type_pack_tag<Types_to_construct...>, Type_pack_tag<move_ref_T1>> ap_caller_part; std::any ap_arg; };
Os links de modelo “destros” não são realmente significados destros. Esses são os chamados "links universais" que, dependendo do tipo de
T1 , se tornam
T1 & ou
T1 && . Portanto, é necessário usar soluções alternativas: primeiro, como as especializações são definidas para ambos os tipos de links (não é bem dito pelas razões já indicadas) e para parâmetros que não são de referência, quando você instancia o modelo, a especialização necessária será selecionada, mesmo que seja um link para destros; segundo, para transferir o tipo
T1 de pacote para pacote,
é usada a versão corrigida de
move_ref_T1 , que é transformada em um link de valor real.
A especialização com um link normal é feita da mesma maneira, com as correções necessárias.
Especialização definitiva
template <typename Func_type, typename... Param_type> class _Tagged_args_binder<Func_type, Type_pack_tag<>, Type_pack_tag<Param_type...>> { public: using Ret_type = std::invoke_result_t<Func_type, Param_type...>; inline _Tagged_args_binder(Func_type func, std::vector<std::any>& args) : ap_func{ func } { } inline auto operator()(Param_type... param) { if constexpr(std::is_same_v<void, decltype(ap_func(std::forward<Param_type>(param)...))>) { ap_func(std::forward<Param_type>(param)...); return; } else { return std::forward<Ret_type>(ap_func(std::forward<Param_type>(param)...)); } } inline auto operator()(Param_type... param) const { if constexpr(std::is_same_v<void, Ret_type>) { ap_func(param...); return; } else { return std::forward<Ret_type>(ap_func(param...)); } } private: Func_type ap_func; };
Essa especialização é responsável por armazenar um objeto funcional e, de fato, é um invólucro sobre ele. É o tipo recursivo final.
Observe como
Type_pack_tag é usado aqui. Todos os tipos de parâmetros agora são compilados no pacote esquerdo. Isso significa que todos eles são processados e empacotados.
Agora, penso, fica claro por que era necessário usar o
Type_pack_tag . O fato é que o idioma não permitiria o uso de dois tipos de pacotes lado a lado, por exemplo, assim:
template <typename Func_type, typename T1, typename... Types_to_construct, typename... Param_type> class _Tagged_args_binder<Func_type, T1, Types_to_construct..., Param_type...> { };
portanto, você deve separá-los em dois pacotes separados dentro de dois tipos. Além disso, é necessário separar os tipos processados daqueles que ainda não foram processados.
Especializações Intermediárias
Das especializações intermediárias, finalmente darei uma especialização, novamente, para tipos de valor, o resto é por analogia:
template <typename Func_type, typename T1, typename... Types_to_construct, typename... Param_type> class _Tagged_args_binder<Func_type, Type_pack_tag<T1, Types_to_construct...>, Type_pack_tag<Param_type...>> { public: using Ret_type = std::invoke_result_t<Func_type, Param_type..., T1, Types_to_construct...>; static_assert(!std::is_same_v<void, T1>, "Void argument is not allowed"); inline _Tagged_args_binder(Func_type func, std::vector<std::any>& args) : ap_arg{ std::move(unihold::reference_any_cast<T1>(args.at(sizeof...(Param_type)))) }, ap_caller_part{ func, args } { } inline auto operator()(Param_type... param) { if constexpr (std::is_same_v<void, Ret_type>) { ap_caller_part(std::forward<Param_type>(param)..., std::move(ap_arg)); return; } else { return std::forward<Ret_type>(ap_caller_part(std::forward<Param_type>(param)..., std::move(ap_arg))); } } inline auto operator()(Param_type... param) const { if constexpr (std::is_same_v<void, Ret_type>) { ap_caller_part(std::forward<Param_type>(param)..., std::move(ap_arg)); } else { return std::forward<Ret_type>(ap_caller_part(std::forward<Param_type>(param)..., std::move(ap_arg))); } } private: _Tagged_args_binder<Func_type, Type_pack_tag<Types_to_construct...>, Type_pack_tag<Param_type..., T1>> ap_caller_part; T1 ap_arg; };
Essa especialização pretende compactar qualquer argumento, exceto o primeiro.
Classe de fichário
A classe
_Tagged_args_binder não se destina ao uso direto, que eu gostaria de enfatizar com um único sublinhado no início de seu nome. Portanto, darei o código de uma classe pequena, que é uma espécie de “interface” para esse tipo feio e inconveniente de usar (que, no entanto, usa truques C ++ bastante incomuns, o que lhe confere algum charme, na minha opinião):
namespace cutecpplib::delegates { template <typename Functor_type, typename... Param_type> class Variadic_args_binder { using binder_type = impl::_Tagged_args_binder<Functor_type, Type_pack_tag<Param_type...>, Type_pack_tag<>>; public: using Ret_type = std::invoke_result_t<binder_type>; inline Variadic_args_binder(Functor_type function, Param_type... param) : ap_tagged_binder{ function, param... } { } inline Variadic_args_binder(Functor_type function, std::vector<std::any>& args) : ap_tagged_binder{ function, args } { } inline auto operator()() { return ap_tagged_binder(); } inline auto operator()() const { return ap_tagged_binder(); } private: binder_type ap_tagged_binder; }; }
Convenção unihold - passando links dentro de std :: any
Um leitor atento deve ter notado que o código usa a função
unihold :: reference_any_cast () . Esta função, assim como seu analógico
unihold :: pointer_any_cast () , é projetado para implementar o contrato de biblioteca: os argumentos que devem ser passados por referência são passados pelo ponteiro para
std :: any .
A função
reference_any_cast sempre retorna uma referência a um objeto, se o próprio objeto é armazenado no contêiner ou apenas um ponteiro para ele. Se
std :: any contiver um objeto, uma referência a esse objeto será retornada dentro do contêiner; se ele contiver um ponteiro, uma referência será retornada ao objeto apontado pelo ponteiro.
Para cada função, existem opções para a constante
std :: any e versões sobrecarregadas para determinar se o contêiner
std :: any possui um objeto ou contém apenas um ponteiro.
As funções precisam ser explicitamente especializadas no tipo de objeto armazenado, assim como conversões do tipo C ++ e funções de modelo semelhantes.
O código para estas funções:
template <typename T> std::remove_reference_t<T>& unihold::reference_any_cast(std::any& wrapper) { bool result; return reference_any_cast<T>(wrapper, result); } template <typename T> const std::remove_reference_t<T>& unihold::reference_any_cast(const std::any& wrapper) { bool result; return reference_any_cast<T>(wrapper, result); } template <typename T> std::remove_reference_t<T>& unihold::reference_any_cast(std::any& wrapper, bool& is_owner) { auto ptr = pointer_any_cast<T>(&wrapper, is_owner); if (!ptr) throw std::bad_any_cast{ }; return *ptr; } template <typename T> const std::remove_reference_t<T>& unihold::reference_any_cast(const std::any& wrapper, bool& is_owner) { auto ptr = pointer_any_cast<T>(&wrapper, is_owner); if (!ptr) throw std::bad_any_cast{ }; return *ptr; } template <typename T> std::remove_reference_t<T>* unihold::pointer_any_cast(std::any* wrapper, bool& is_owner) { using namespace std; using NR_T = remove_reference_t<T>;
Conclusão
Tentei descrever brevemente uma das abordagens possíveis para resolver o problema de chamadas de funções dinâmicas em C ++. Posteriormente, isso formará a base da biblioteca de delegados C ++ (na verdade, eu já escrevi a principal funcionalidade da biblioteca, a saber, delegados polimórficos, mas a biblioteca ainda precisa ser reescrita como deveria, para poder demonstrar o código e adicionar algumas funcionalidades não realizadas). Em um futuro próximo, planejo concluir o trabalho na biblioteca e contar exatamente como eu implementei o restante da funcionalidade de delegação em C ++.
PS O uso do RTTI será demonstrado na próxima parte.