C ++ é uma linguagem confusa e sua principal desvantagem é a dificuldade de criar blocos isolados de código. Em um projeto típico, tudo depende de tudo. Este artigo mostra como escrever código altamente isolado que depende minimamente de bibliotecas específicas (incluindo padrão), implementações, reduzindo a dependência de qualquer parte do código em um conjunto de interfaces. Além disso, serão propostas soluções arquitetônicas para parametrização de código, que podem interessar não apenas programadores C ++, mas também programadores Java. E o que é importante, a solução proposta é muito econômica em termos de tempo de desenvolvimento.
Isenção de responsabilidade : neste artigo, reuni minhas idéias sobre arquitetura ideal. Algumas idéias não são minhas (mas não me lembro de quem), algumas são comuns e conhecidas por todos - isso não é importante, porque não ofereço minhas próprias idéias sobre uma boa arquitetura, mas um código específico que permitirá que essa arquitetura seja abordada a um preço mínimo.
Isenção de responsabilidade N2 :
Ficarei feliz com o feedback construtivo expresso em palavras. Se você entende pior do que eu e me repreende, significa que em algum lugar não expliquei o suficiente e faz sentido refazer o texto. Se você entende melhor do que eu, significa que vou ganhar uma experiência valiosa. Agradecemos antecipadamente.
Isenção de responsabilidade N3 : escrevi grandes aplicativos a partir do zero, mas não escrevi aplicativos corporativos para servidores e clientes. Tudo é diferente lá e, provavelmente, minha experiência parecerá estranha para especialistas neste campo. E o artigo não é sobre isso, os mesmos problemas de escalabilidade não são considerados aqui.
Isenção de responsabilidade N4 (
Atualização. Com base nos comentários): Alguns comentaristas sugeriram que eu reinventasse o Fowler e ofereça padrões de design conhecidos. Definitivamente não é esse o caso. Proponho uma ferramenta de parametrização muito pequena que permite implementar esses padrões com um mínimo de rabisco. Incluindo o Localizador de Serviços e Injeção de Dependência da Fowler, mas não apenas - usando a classe TypedSet, você também pode implementar economicamente um conjunto de estratégias. Nesse caso, o Fowler acessou através de linhas, o que é caro - minha ferramenta de custo zero, custo zero (se absolutamente estritamente, então log (N) em vez de 2M * log (N), onde M é o comprimento da cadeia de parâmetros para o Localizador de serviços. após o aparecimento do constexpr typeid em c ++ 20, o preço deve ficar completamente zero). Portanto, peço que você não estenda o significado do artigo aos padrões de design. Aqui você encontrará apenas um
método para a implementação barata desses padrões.
Os exemplos serão em C ++, mas todas as opções acima são bastante implementáveis em Java. Talvez, com o tempo, eu forneça código de trabalho para Java se a solicitação para isso estiver nos comentários de você.
Parte 1. Arquitetura esférica no vácuo
Antes de resolver brilhantemente todas as dificuldades, você precisa criá-las corretamente. Com maestria criando dificuldades para si mesmo no lugar certo, você pode facilitar bastante a solução deles. Para isso, formulamos um objetivo cuja solução vamos apresentar métodos - os princípios mínimos da boa arquitetura.
De fato, a mágica da boa arquitetura é apenas dois princípios, e o que está escrito abaixo é apenas uma decodificação. O primeiro princípio é a testabilidade do código. A testabilidade é como o fio de Ariadne que leva a uma boa arquitetura. Se você não sabe como escrever um teste de funcionalidade, arruinou a arquitetura. Se você não sabe como criar uma boa arquitetura, pense em qual será o teste para a funcionalidade que você planejou - e você criará automaticamente uma barra de qualidade arquitetural para si mesmo e bastante alta. Os pensamentos nos testes aumentam automaticamente a modularidade, diminuem a conectividade e tornam a arquitetura mais lógica.
E eu não quero dizer TDD. Uma doença típica de muitos programadores é o culto religioso de tecnologias lidas em algum lugar sem entender os limites de sua eficácia. O TDD é bom quando vários programadores estão trabalhando no código, quando há um departamento de testes e as autoridades entendem por que boas práticas de codificação são necessárias e estão prontas para pagar não apenas por algum código que resolve o problema, mas também por sua confiabilidade. Se seus superiores não estiverem prontos para pagar, você terá que trabalhar mais economicamente. No entanto, você ainda precisa testar o código - a menos que, é claro, tenha um senso de autopreservação.
O segundo princípio é modularidade. Mais precisamente, modularidade altamente isolada sem o uso de bibliotecas / código rígido que não estão relacionados ao próprio módulo. Agora, ao projetar arquiteturas de servidor, está na moda dividir um monólito em microsserviços. Vou lhe contar um segredo terrível - cada módulo no monólito deve ser como um microsserviço. No sentido de que ele deve se destacar facilmente do código geral com um mínimo de cabeçalhos conectados no ambiente de teste. Ainda não está claro, mas vou explicar com um exemplo: Você já tentou alocar shared_ptr do boost? Se ao mesmo tempo você conseguir arrastar não apenas todo o impulso, mas apenas metade de suas matérias-primas, isso significa que você matou de três a cinco dias para eliminar vícios desnecessários !!! Ao mesmo tempo, você arrasta o fato de que shared_ptr definitivamente não tem nada a ver !!!
E é pior que um erro - é um crime arquitetônico.
Com uma boa arquitetura, você deve poder remover o shared_ptr, substituindo sem problemas e rapidamente tudo o que não está relacionado ao shared_ptr pelas versões de teste. Por exemplo, uma versão de teste do alocador. Ou esqueça o impulso. Digamos que você escreva um analisador xml / html. Você precisa trabalhar com seqüências de caracteres e arquivos com o analisador. E se estivermos falando de uma arquitetura ideal que não esteja ligada às necessidades de uma empresa de produção / software específica, para um analisador com uma arquitetura ideal, não temos o direito de usar as operações de pesquisa std :: istream, std :: file_system, std :: string e hardcode com strings no analisador. Precisamos fornecer uma interface de fluxo, uma interface para operações de arquivo (talvez dividida em subinterfaces, mas o acesso a subinterfaces ainda precisará ser feito através da interface do módulo de operações de arquivo), uma interface para trabalhar com strings, uma interface de alocador e, idealmente, também uma interface para a própria linha. Como resultado, podemos substituir sem problemas tudo o que não está relacionado à análise por espaços em branco de teste ou inserir uma versão de teste do alocador / trabalhar com pesquisa de arquivos / string por verificações adicionais. E a versatilidade da solução aumentará - amanhã, sob a interface do fluxo, não haverá um arquivo, mas um site em algum lugar da Internet, e ninguém notará. Você pode substituir a biblioteca padrão pelo Qt e, em seguida, alternar para o visual c ++ e começar a usar apenas coisas do Linux - e as alterações serão mínimas. Como spoiler, direi que, com essa abordagem, a questão do preço surge em pleno crescimento - cobrir tudo com interfaces, incluindo elementos da biblioteca padrão, é caro, mas isso não é um objetivo, mas uma solução.
Em geral, o princípio radical de módulo como microsserviço, proclamado neste artigo, é um ponto sensível em C ++ e geralmente é um código positivo. Se você criar arquivos de declaração e separar interfaces separadamente das implementações, ainda poderá criar independência / isolamento de arquivos cpp um do outro e, em seguida, relativo, não 100%, os cabeçalhos geralmente são tecidos em um monólito sólido, do qual nada pode ser arrancado sem carne. E embora isso tenha um efeito terrível no tempo de compilação, é. Além disso, mesmo que a independência dos títulos seja alcançada, isso significa automaticamente a incapacidade de agregar classes. Na verdade, a única maneira de obter independência dos arquivos .cpp e dos cabeçalhos no c ++ é declarar classes pré-usadas (sem defini-las) e depois usar apenas ponteiros para elas. assim que você usar a própria classe em vez do ponteiro de classe no arquivo de cabeçalho (ou seja, agregá-la), você criará um monte de todos os .cpp-shniks que incluirão esse cabeçalho e o .cpp-shnik que contém a definição de classe. Ainda existe o fastpimpl, mas apenas garantimos a criação de dependências no nível do cpp.
Portanto, para uma boa arquitetura, o isolamento de módulos é importante - a capacidade de extrair um módulo com o primeiro cabeçalho conectando macros e os principais tipos de biblioteca, com um segundo cabeçalho para declarações e várias inclusões conectando um conjunto de interfaces. E apenas o que se relaciona com essa funcionalidade, e todo o resto deve ser armazenado em outros módulos e acessível apenas através de interfaces.
Declaramos as principais características da boa arquitetura, incluindo os pontos indicados acima, ponto a ponto.
Vamos definir o termo "Módulo". Um módulo é a soma das funcionalidades logicamente relacionadas. Por exemplo, trabalhe com fluxos ou trabalho de arquivo ou um analisador de html.
O módulo “File Work” pode combinar muitas funcionalidades - abra um arquivo, feche, posicione, leia propriedades, leia o tamanho do arquivo. Ao mesmo tempo, o scanner de pasta pode ser projetado como parte da interface "Trabalho de arquivo" ou como um módulo separado, e o trabalho com fluxos pode ser colocado em um módulo separado, com certeza. O que, no entanto, não interfere na organização do acesso a todos os outros módulos aos fluxos e ao scanner de pastas indiretamente, através do "Trabalho de Arquivo". Isso não é necessário, mas bastante lógico.
- Modularidade. "Módulo como microsserviço" imperativo.
- Alocação de 20% do código executado 80% do tempo em uma biblioteca separada - o núcleo do programa
- Testabilidade de cada funcionalidade de cada módulo
- Interface, é a falta de código rígido. Você só pode chamar o código rígido diretamente relacionado à funcionalidade do módulo e deve fazer as outras chamadas diretas da biblioteca para um módulo separado e acessá-las por meio da interface.
- Isolamento completo do módulo por interfaces do ambiente externo. A proibição de "pregar" implementações que não estão relacionadas à funcionalidade da classe. E, mais radicalmente, isolar bibliotecas (incluindo as padrão) com interfaces / adaptadores / decoradores
- A agregação de uma classe ou a criação de uma variável de classe ou fastpimpl é usada apenas quando é essencial para o desempenho.
Obviamente, descobriremos como conseguir tudo isso rapidamente por um preço mais baixo, mas eu gostaria de chamar a atenção para outro problema, cuja solução será um bônus para nós - a transferência de parâmetros dependentes da plataforma. Por exemplo, se você precisar criar um código que funcione igualmente no Android e no Windows, será lógico alocar algoritmos dependentes da plataforma em módulos separados. Nesse caso, provavelmente, a implementação para o Android pode exigir uma referência ao ambiente Java (jni), JNIEnv *, e possivelmente alguns objetos Java. E a implementação no Windows pode exigir uma pasta de trabalho do programa (que no android pode ser solicitada ao sistema, com JNIEnv *). O truque é que o mesmo JNIEnv * não existe no contexto do Windows, portanto, mesmo uma união digitada ou sua alternativa c ++ à std :: variant é impossível. Obviamente, você pode usar o vetor void * ou o vetor std :: any como parâmetro, mas honestamente, essa é uma muleta atípica. Atípico - porque rejeita a principal vantagem do c ++, digitação forte. E isso é mais perigoso que o SARS.
Além disso, analisaremos como resolver esse problema de maneira estritamente tipificada.
Parte 2. Balas mágicas e seu preço
Então, digamos que temos uma grande quantidade de código que precisa ser escrita do zero, e o resultado será um projeto muito grande.
Como pode ser montado de acordo com os princípios que determinamos?
A maneira clássica, aprovada por todos os manuais, é dividir tudo em interfaces e estratégias. Com a ajuda de interfaces e estratégias, se houver muitas delas, qualquer subproblema do nosso projeto pode ser isolado a tal ponto que o princípio "módulo como microsserviço" começará a trabalhar nele. Mas minha experiência pessoal é que, se você dividir o projeto em 20 a 30 partes, que será isolado no nível de "módulo como microsserviço", terá êxito. Mas a principal característica da boa arquitetura é a capacidade de testar qualquer classe fora do contexto do projeto. E se você já isola cada classe, já existem mais de 500 módulos e, na minha experiência, isso aumenta o tempo de desenvolvimento em 3-5 vezes, o que significa que em "condições de combate" você não fará isso e comprometerá preço e qualidade.
Alguém pode duvidar, e estará em seu próprio direito. Vamos fazer uma estimativa aproximada. Deixe a classe média ter 3-5 membros e 20 funções e 3 construtores. Além disso, 6 a 10 getters e setters (mutadores) para acesso aos nossos membros. Total de cerca de 40 unidades na classe. Em um projeto típico, cada classe de “centro” precisa acessar uma média de cinco funcionalidades, não um centro para 3. Por exemplo, muitas classes precisam de um alocador, sistema de arquivos, trabalho com seqüências de caracteres, trabalho com fluxos e acesso a bancos de dados.
Cada estratégia / interface exigirá um membro do tipo
std::shared_ptr<CreateStreamStrategy> m_create_stream;
. Dois mutadores, além de inicialização em cada um dos três construtores. Além disso, em algum ponto da inicialização de nossa classe, você precisará chamar algo como
myclass->SetCreateStreamStrategy( my_create_stream_strategy )
algumas vezes, para um total de 8 unidades por interface / estratégia e, como temos cerca de cinco, haverá 40 unidades. Ou seja, tornamos a classe de origem duas vezes mais complicada. E a perda de simplicidade afetará inevitavelmente a legibilidade, e em algum outro lugar do processo de depuração e meia, apesar do fato de que nada parece ter mudado essencialmente.
Então a questão é. Como fazer o mesmo, mas a um preço mínimo? A primeira coisa que vem à mente é a parametrização estática nos modelos, no estilo do Alexandrescu e da biblioteca Loki.
Estamos escrevendo uma aula em grande estilo
template < struct Traits > class MyClass { public: void DoMainTaskFunction() { ... MyStream stream = Traits::streamwork::Open( stream_name ); ... } };
Essa decisão tem todas as vantagens arquiteturais que identificamos na primeira parte. Mas também há muitas desvantagens.
Eu mesmo gosto de transbordar, mas me arrependo de admitir: os modelos no código comum são amados apenas pelos mágicos dos modelos. Uma massa significativa de programadores com a palavra "modelo" franze a testa levemente. Além disso, na indústria, a grande maioria das vantagens não possui vantagens, mas é um pouco retreinada em syshniks em c ++ que não têm conhecimento profundo das vantagens, mas se enquadram na palavra "modelo" e fingem estar mortos.
Se traduzirmos isso em uma linguagem de produção, manter o código na parametrização estática é mais caro e mais complicado.
Ao mesmo tempo, se quisermos, para propósitos de maior legibilidade, remover cuidadosamente o corpo da função fora da classe, obteremos um monte de rabiscos com os nomes dos modelos e parâmetros do modelo. E, no caso de um erro de compilação, obtemos longas prateleiras legíveis por humanos de áreas de causas e problemas com vários modelos aninhados complexos.
Mas, existe uma saída simples. Como mágico de modelo, declaro que quase tudo o que pode ser feito usando parametrização estática / polimorfismo estático pode ser transferido para polimorfismo dinâmico. Não, é claro, não erradicaremos o modelo mal até o fim - mas não o espalharemos com uma mão generosa para parametrização em cada classe, mas o limitaremos a algumas aulas instrumentais.
Parte três. A solução proposta e o código codificado para esta solução
Então LÁ !!! Conheça a classe de modelo TypedSet. Ele associa um ponteiro inteligente desse tipo a um único tipo. Além disso, para o tipo especificado, ele pode ter um objeto, mas não pode. Como não gosto do nome, ficarei grato se nos comentários me indicar uma opção mais bem-sucedida.
Um tipo - um objeto. Mas o número de tipos não é limitado! Portanto, você pode passar uma classe como um parametrizador.
Quero chamar sua atenção para um ponto. Pode parecer que em algum momento você pode precisar de dois objetos em uma interface. De fato, se essa necessidade surgir, então (na minha opinião) isso significa um erro de arquitetura. Ou seja, se você tiver dois objetos em uma interface, eles não serão mais interfaces de acesso funcional: essas são variáveis de entrada para a função ou você não possui uma, mas duas funcionalidades às quais precisa acessar, é melhor dividir a interface em duas .
Faremos três funções básicas: Criar, Obter e Tem. Por conseguinte, a criação, recebimento e verificação da presença de um elemento.
A propósito, vi uma solução alternativa de colegas escrevendo no Qt. Lá, o acesso à interface desejada era realizado por meio de um singleton, que "mapeou" a interface desejada, empacotada no Varaint, por meio de uma linha de texto (!!!) e, após lançar essa opção, o resultado poderia ser utilizado.
GlobalConfigurator()["FileSystem"].Get().As<FileSystem>()
Certamente funciona, mas a sobrecarga de contar o comprimento e mais amarrar a corda é um pouco assustadora para minha alma otimista. Aqui, a sobrecarga é zero, porque a escolha da interface desejada é realizada em tempo de compilação.
Com base no TypedSet, podemos criar a classe StrategiesSet, que já é mais avançada. Nele, armazenaremos não apenas um objeto por interface de acesso para cada funcional, mas também para cada interface (daqui em diante denominada estratégia) um TypedSet adicional com parâmetros para essa estratégia. Esclareço: os parâmetros, diferentemente das variáveis de função, são definidos uma vez durante a inicialização do programa ou uma vez para uma grande execução do programa. Os parâmetros permitem que você torne o código realmente multiplataforma. É neles que dirigimos toda a cozinha dependente da plataforma.
Aqui teremos funções mais básicas: Create, Get, CreateParamsSet e GetParamsSet. Não foi estabelecido, porque é arquitetonicamente redundante: se o seu código se refere à funcionalidade de trabalhar com o sistema de arquivos, mas o código de chamada não o forneceu, você só pode lançar uma exceção ou afirmar, ou
fazer com que o programa sebukka chame a função abort ().
class StrategiesSet { public: template <class Strategy> void Create( const std::shared_ptr<Strategy> & value ); template <class Strategy> std::shared_ptr<Strategy> Get(); template <class Strategy> void CreateParamsSet(); template <class Strategy> std::shared_ptr<TypedSet> GetParamsSet(); template <class Strategy, class ParamType> void CreateParam( const std::shared_ptr<ParamType> & value ); template <class Strategy, class ParamType> std::shared_ptr<ParamType> GetParam(); protected: TypedSet const & strategies() const { return strategies_; } TypedSet & get_strategies() { return strategies_; } TypedSet const & params() const { return params_; } TypedSet & get_params() { return params_; } template <class Type> struct ParamHolder { ParamHolder( ) : param_ptr( std::make_shared<TypedSet>() ) {} std::shared_ptr<TypedSet> param_ptr; }; private: TypedSet strategies_; TypedSet params_; }; template <class Strategy> void StrategiesSet::Create( const std::shared_ptr<Strategy> & value ) { get_strategies().Create<Strategy>( value ); } template <class Strategy> std::shared_ptr<Strategy> StrategiesSet::Get() { return get_strategies().Get<Strategy>(); } template <class Strategy> void StrategiesSet::CreateParamsSet( ) { typedef ParamHolder<Strategy> Holder; std::shared_ptr< Holder > ptr = std::make_shared< Holder >( ); ptr->param_ptr = std::make_shared< TypedSet >(); get_params().Create< Holder >( ptr ); } template <class Strategy> std::shared_ptr<TypedSet> StrategiesSet::GetParamsSet() { typedef ParamHolder<Strategy> Holder; if ( get_params().Has< Holder >() ) { return get_params().Get< Holder >()->param_ptr; } else { LogError("StrategiesSet::GetParamsSet : get unexisting!!!"); return std::shared_ptr<TypedSet>(); } } template <class Strategy, class ParamType> void StrategiesSet::CreateParam( const std::shared_ptr<ParamType> & value ) { typedef ParamHolder<Strategy> Holder; if ( !params().Has<Holder>() ) CreateParamsSet<Strategy>(); if ( params().Has<Holder>() ) { std::shared_ptr<TypedSet> params_set = GetParamsSet<Strategy>(); params_set->Create<ParamType>( value ); } else { LogError( "Param creating error: Access Violation" ); } } template <class Strategy, class ParamType> std::shared_ptr<ParamType> StrategiesSet::GetParam() { typedef ParamHolder<Strategy> Holder; if ( params().Has<Holder>() ) { return GetParamsSet<Strategy>()->template Get<ParamType>();
Uma vantagem adicional é que, no estágio de criação de protótipos, você pode criar uma classe de digitação super grande, acessar acesso a todos os módulos nela e passá-la a todos os módulos como parâmetro, rapidamente se tornar pequena e, em seguida, dividi-la silenciosamente em partes que são minimamente necessárias para cada módulo.
Bem, e um caso de uso pequeno e (ainda) excessivamente simplificado. Espero que você nos comentários me sugira o que você gostaria de ver como um exemplo simples, e farei do artigo uma pequena atualização. Como diz a sabedoria popular de programação, “libere o mais cedo possível e aprimore o uso de feedback após o lançamento”.
class Interface1 { public: virtual void Fun() { printf("\niface1\n");} virtual ~Interface1() {} }; class Interface2 { public: virtual void Fun() { printf("\niface2\n");} virtual ~Interface2() {} }; class Interface3 { public: virtual void Fun() { printf("\niface3\n");} virtual ~Interface3() {} }; class Implementation1 : public Interface1 { public: virtual void Fun() override { printf("\nimpl1\n");} }; class Implementation2 : public Interface2 { public: virtual void Fun() override { printf("\nimpl2\n");} }; class PrintParams { public: virtual ~PrintParams() {} virtual std::string GetOs() = 0; }; class PrintParamsUbuntu : public PrintParams { public: virtual std::string GetOs() override { return "Ubuntu"; } }; class PrintParamsWindows : public PrintParams { public: virtual std::string GetOs() override { return "Windows"; } }; class PrintStrategy { public: virtual ~PrintStrategy() {} virtual void operator() ( const TypedSet& params, const std::string & str ) = 0; }; class PrintWithOsStrategy : public PrintStrategy { public: virtual void operator()( const TypedSet& params, const std::string & str ) override { auto os = params.Get< PrintParams >()->GetOs(); printf(" Printing: %s (OS=%s)", str.c_str(), os.c_str() ); } }; void TestTypedSet() { using namespace std; TypedSet a; a.Create<Interface1>( make_shared<Implementation1>() ); a.Create<Interface2>( make_shared<Implementation2>() ); a.Get<Interface1>()->Fun(); a.Get<Interface2>()->Fun(); Log("Double creation:"); a.Create<Interface1>( make_shared<Implementation1>() ); Log("Get unexisting:"); a.Get<Interface3>(); } void TestStrategiesSet() { using namespace std; StrategiesSet printing; printing.Create< PrintStrategy >( make_shared<PrintWithOsStrategy>() ); printing.CreateParam< PrintStrategy, PrintParams >( make_shared<PrintParamsWindows>() ); auto print_strategy_ptr = printing.Get< PrintStrategy >(); auto & print_strategy = *print_strategy_ptr; auto & print_params = *printing.GetParamsSet< PrintStrategy >(); print_strategy( print_params, "Done!" ); } int main() { TestTypedSet(); TestStrategiesSet(); return 0; }
Sumário
Assim, resolvemos um problema importante: deixamos na turma apenas a interface diretamente relacionada à funcionalidade da turma. O resto foi "empurrado" para o StrategiesSet, evitando tanto a bagunça da classe com elementos desnecessários quanto "pregando" certas funcionalidades necessárias aos algoritmos. Isso nos permitirá não apenas escrever código altamente isolado, com zero dependências de implementações e bibliotecas, mas também economizar uma quantidade enorme de tempo.
O código para as classes de exemplo e ferramenta pode ser encontrado
aqui.Upd. a partir de 13/11/2019De fato, o código mostrado aqui é apenas um exemplo simplificado de legibilidade. O fato é que typeid () .Hash_code é implementado em compiladores modernos lenta e ineficientemente. Seu uso mata muito do significado. Além disso, como o respeitado
0xd34df00d sugeriu , o padrão não garante a capacidade de distinguir tipos por
código hash (na prática, essa abordagem, no entanto, funciona). Mas o exemplo é bem lido. Reescrevi TypedSet sem typeid (). Hash_code (), além disso, substitui o mapa por array (mas com a capacidade de alternar rapidamente de mapa para array e vice-versa alterando um dígito em #if). Acabou sendo mais difícil, mas mais interessante para uso prático.
em coliru namespace metatype { struct Counter { size_t GetAndIncrease() { return counter_++; } private: size_t static inline counter_ = 1; }; template <typename Type> struct HashGetterBody { HashGetterBody() : hash_( counter_.GetAndIncrease() ) { } size_t GetHash() { return hash_; } private: Counter counter_; size_t hash_; }; template <typename Type> struct HashGetter { size_t GetHash() {return hasher_.GetHash(); } private: static inline HashGetterBody<Type> hasher_; }; }
Aqui o acesso é realizado em tempo linear, os hashes de tipo são contados antes que main () seja lançado, as perdas são apenas para verificações de validação, que podem ser descartadas, se desejado.