C ++ es un lenguaje confuso, y su mayor inconveniente es la dificultad de crear bloques de código aislados. En un proyecto típico, todo depende de todo. Este artículo muestra cómo escribir código altamente aislado que depende mínimamente de bibliotecas específicas (incluidas las estándar), implementaciones, reduciendo la dependencia de cualquier parte del código a un conjunto de interfaces. Además, se propondrán soluciones arquitectónicas para la parametrización del código, que pueden interesar no solo a los programadores de C ++, sino también a los programadores de Java. Y lo que es importante, la solución propuesta es muy económica en términos de tiempo de desarrollo.
Descargo de responsabilidad : en este artículo he reunido mis ideas sobre la arquitectura ideal. Algunas ideas no son mías (pero no recuerdo cuáles), algunas ideas son comunes y son conocidas por todos; esto no es importante, porque no ofrezco mis ideas sobre una buena arquitectura, sino un código específico que permitirá que esta arquitectura se aborde a un precio mínimo.
Descargo de responsabilidad N2 : estaré contento con la retroalimentación constructiva expresada en palabras. Si entiendes peor que yo y me regañas, significa que en algún lugar no lo he explicado con suficiente claridad, y tiene sentido volver a trabajar el texto. Si entiendes mejor que yo, significa que obtendré una valiosa experiencia. Gracias de antemano.
Descargo de responsabilidad N3 : escribí grandes aplicaciones desde cero, pero no escribí aplicaciones empresariales de servidor y cliente. Allí todo es diferente y, probablemente, mi experiencia les parecerá extraña a los especialistas en este campo. Y el artículo no trata sobre eso, los mismos problemas de escalabilidad no se consideran aquí en absoluto.
Descargo de responsabilidad N4 (
Upd. Basado en comentarios): Algunos comentaristas han sugerido que reinvente Fowler y ofrezca patrones de diseño conocidos desde hace mucho tiempo. Este definitivamente no es el caso. Propongo una herramienta de parametrización muy pequeña que le permite implementar estos patrones con un mínimo de garabatos. Incluyendo el Inyector de dependencias de Fowler y el Localizador de servicios, pero no solo: con la clase TypedSet también puede implementar un conjunto de estrategias de manera económica. En este caso, Fowler accedió a través de líneas, lo cual es costoso: mi herramienta de costo cero, costo cero (si es estrictamente estricto, luego log (N) en lugar de 2M * log (N), donde M es la longitud de la cadena de parámetros para el Localizador de servicios. después de la aparición de constexpr typeid en c ++ 20, el precio debería ser completamente cero). Por lo tanto, le pido que no extienda el significado del artículo para diseñar patrones. Aquí encontrará solo un
método para la implementación económica de estos patrones.
Los ejemplos estarán en C ++, pero todo lo anterior es bastante implementable en Java. Quizás, con el tiempo, le daré un código de trabajo para Java si la solicitud para esto estará en sus comentarios.
Parte 1. Arquitectura esférica en el vacío.
Antes de resolver brillantemente todas las dificultades, debe crearlas correctamente. Creando magistralmente dificultades para usted en el lugar correcto, puede facilitar enormemente su solución. Para esto, formulamos una meta para la solución de la cual propondremos métodos: los principios mínimos de una buena arquitectura.
De hecho, la magia de la buena arquitectura son solo dos principios, y lo que está escrito a continuación es solo una decodificación. El primer principio es la capacidad de prueba del código. La capacidad de prueba es como el hilo de Ariadne que te lleva a una buena arquitectura. Si no sabe cómo escribir una prueba de funcionalidad, ha arruinado la arquitectura. Si no sabe cómo crear una buena arquitectura, piense en cuál será la prueba para la funcionalidad que planeó, y automáticamente creará una barra de calidad arquitectónica para usted y bastante alta. Los pensamientos sobre las pruebas aumentan automáticamente la modularidad, reducen la conectividad y hacen que la arquitectura sea más lógica.
Y no me refiero a TDD. Una enfermedad típica de muchos programadores es el culto religioso a las tecnologías leídas en alguna parte sin comprender los límites de su efectividad. TDD es bueno cuando varios programadores están trabajando en el código, cuando hay un departamento de pruebas y las autoridades entienden por qué se necesitan buenas prácticas de codificación y está dispuesto a pagar no solo por algún código que resuelva el problema, sino también por su confiabilidad. Si sus superiores no están listos para pagar, tendrá que trabajar de manera más económica. Sin embargo, aún debe probar el código, a menos que, por supuesto, tenga una sensación de autoconservación.
El segundo principio es la modularidad. Más precisamente, modularidad altamente aislada sin el uso de bibliotecas / hardcode que no están relacionados con el módulo en sí. Ahora, al diseñar arquitecturas de servidor, está de moda dividir un monolito en microservicios. Te diré un terrible secreto: cada módulo del monolito debería ser como un microservicio. En el sentido de que debería destacarse fácilmente del código general con un mínimo de encabezados conectados en el entorno de prueba. Todavía no está claro, pero lo explicaré con un ejemplo: ¿Alguna vez ha tratado de asignar shared_ptr de un impulso? ¡Si al mismo tiempo logras arrastrar no solo todo el impulso, sino solo la mitad de sus materias primas, entonces esto significa que mataste de tres a cinco días para eliminar adicciones innecesarias! Al mismo tiempo, arrastra el hecho de que shared_ptr definitivamente no tiene nada que hacer.
Y es peor que un error: es un crimen arquitectónico.
Con una buena arquitectura, debería poder arrancar shared_ptr, reemplazando sin problemas y rápidamente todo lo que no esté relacionado con shared_ptr con versiones de prueba. Por ejemplo, una versión de prueba del asignador. O olvídate del impulso. Supongamos que escribe un analizador xml / html. Debe trabajar con cadenas y trabajar con archivos para el analizador. Y si estamos hablando de una arquitectura ideal que no está vinculada a las necesidades de una compañía de producción / software en particular, entonces, para un analizador con una arquitectura ideal, no tenemos el derecho de usar std :: istream, std :: file_system, std :: cadena y operaciones de búsqueda de código con cadenas. en el analizador Debemos proporcionar una interfaz de flujo, una interfaz para operaciones de archivo (tal vez dividida en subinterfaces, pero el acceso a las subinterfaces todavía tendrá que hacerse a través de la interfaz del módulo de operaciones de archivo), una interfaz para trabajar con cadenas, una interfaz de asignador, e idealmente también una interfaz para la línea misma. Como resultado, podemos reemplazar sin problemas todo lo que no esté relacionado con el análisis con espacios en blanco de prueba, o insertar una versión de prueba del asignador / trabajar con archivos / búsqueda de cadenas con verificaciones adicionales. Y la versatilidad de la solución aumentará: mañana, bajo la interfaz de la transmisión, no habrá un archivo, sino un sitio en algún lugar de Internet, y nadie lo notará. Puede reemplazar la biblioteca estándar con Qt, y luego cambiar a visual c ++, y luego comenzar a usar solo cosas de Linux, y las alteraciones serán mínimas. Como spoiler, diré que con este enfoque, la cuestión del precio surge en pleno crecimiento: cubrir todo con interfaces, incluidos elementos de la biblioteca estándar, es costoso, pero esto no es un objetivo, sino una solución.
En general, el principio radical del módulo como microservicio proclamado en este artículo es un punto doloroso en C ++ y, en general, el código más típico. Si crea archivos de declaración e interfaces separadas por separado de las implementaciones, aún puede crear independencia / aislamiento de los archivos cpp entre sí, y luego, en relación, no al 100%, los encabezados generalmente se entrelazan en un monolito sólido, del que nada puede extraerse sin carne. Y aunque esto tiene un efecto terrible en el tiempo de compilación, lo es. Además, incluso si se logra la independencia de los encabezados, esto automáticamente significa la incapacidad de agregar clases. En realidad, la única forma de lograr la independencia de los archivos .cpp y los encabezados en c ++ es declarar las clases usadas anteriormente (sin definirlas) y luego usar solo punteros a ellas. tan pronto como use la clase en lugar del puntero de clase en el archivo de encabezado (es decir, agregue), creará un montón de todos los .cpp-shniks que incluyen este encabezado y ese .cpp-shnik que contiene la definición de clase. Todavía hay fastpimpl, pero solo se garantiza la creación de dependencias en el nivel de cpp.
Entonces, para una buena arquitectura, el aislamiento de los módulos es importante: la capacidad de extraer un módulo con el primer encabezado que conecta las macros y los tipos principales de biblioteca, con un segundo encabezado para las declaraciones y varias inclusiones que conectan un conjunto de interfaces. Y solo lo que se relaciona con esta funcionalidad, y todo lo demás debe almacenarse en otros módulos y ser accesible solo a través de las interfaces.
Enunciamos las características principales de una buena arquitectura, incluidos los puntos indicados anteriormente, punto por punto.
Definamos el término "Módulo". Un módulo es la suma de funcionalidades lógicamente relacionadas. Por ejemplo, trabaje con secuencias o trabajo de archivos, o un analizador html.
El módulo "File Work" puede combinar muchas funcionalidades: abrir un archivo, cerrar, colocar, leer propiedades, leer el tamaño del archivo. Al mismo tiempo, el escáner de carpetas se puede diseñar como parte de la interfaz "File Work", o como un módulo separado, y el trabajo con flujos se puede colocar en un módulo separado con seguridad. Lo cual, sin embargo, no interfiere con la organización del acceso a todos los demás módulos a las transmisiones y al escáner de carpetas indirectamente, a través del "Trabajo de archivo". Esto no es necesario, pero es bastante lógico.
- Modularidad Imperativo "Módulo como microservicio".
- Asignación del 20% del código ejecutado el 80% del tiempo en una biblioteca separada: el núcleo del programa
- Capacidad de prueba de cada funcionalidad de cada módulo
- Interfaz, es la falta de código duro. Solo puede llamar al código duro que está directamente relacionado con la funcionalidad del módulo, y debe hacer las otras llamadas directas de la biblioteca a un módulo separado y acceder a ellas a través de la interfaz.
- Aislamiento completo del módulo por interfaces del entorno externo. La prohibición de implementaciones de "clavado" que no están relacionadas con la funcionalidad de la clase. Y más radicalmente, aislando bibliotecas (incluidas las estándar) con interfaces / adaptadores / decoradores
- Agregar una clase o crear una variable de clase o fastpimpl se usa solo cuando es crítico para el rendimiento.
Por supuesto, descubriremos cómo lograr todo esto rápidamente por un precio más bajo, pero me gustaría llamar la atención sobre otro problema, cuya solución será una ventaja para nosotros: la transferencia de parámetros dependientes de la plataforma. Por ejemplo, si necesita crear un código que funcione igualmente en Android y Windows, entonces será lógico asignar algoritmos dependientes de la plataforma en módulos separados. En este caso, probablemente, la implementación para Android puede requerir una referencia al entorno Java (jni), JNIEnv *, y posiblemente un par de objetos Java. Y la implementación en Windows puede requerir una carpeta de trabajo del programa (que en Android se puede solicitar desde el sistema, teniendo JNIEnv *). El truco es que el mismo JNIEnv * no existe en el contexto de Windows, por lo que incluso una unión tipeada o su alternativa de c ++ a std :: variant es imposible. Por supuesto, puede usar el vector void * o std :: any vector como parámetro, pero, sinceramente, esta es una muleta atípica. Atípico: porque rechaza la principal ventaja de c ++, la tipificación fuerte. Y esto es más peligroso que el SARS.
Además, analizaremos cómo resolver este problema de manera estrictamente tipificada.
Parte 2. Balas mágicas y su precio
Entonces, digamos que tenemos una gran cantidad de código que debe escribirse desde cero, y el resultado será un proyecto muy grande.
¿Cómo se puede ensamblar de acuerdo con los principios que hemos determinado?
La forma clásica, aprobada por todos los manuales, es dividir todo en interfaces y estrategias. Con la ayuda de interfaces y estrategias, si hay muchas de ellas, cualquier subtarea de nuestro proyecto puede aislarse de tal manera que el principio "módulo como microservicio" comience a funcionar en él. Pero mi experiencia personal es que si divide el proyecto en 20-30 partes, que se aislarán al nivel de "módulo como microservicio", entonces tendrá éxito. Pero la característica principal de una buena arquitectura es la capacidad de probar cualquier clase fuera del contexto del proyecto. Y si ya aislas cada clase, entonces ya hay más de 500 módulos, y en mi experiencia, esto aumenta el tiempo de desarrollo en 3-5 veces, lo que significa que en "condiciones de combate" no harás esto y comprometerás el precio y la calidad.
Alguien puede dudar, y estará en su propio derecho. Hagamos un cálculo aproximado. Deje que la clase media tenga 3-5 miembros y 20 funciones y 3 constructores. Además de 6-10 getters y setters (mutators) para acceder a nuestros miembros. Total de aproximadamente 40 unidades en la clase. En un proyecto típico, cada clase de "centro" necesita acceso a un promedio de cinco funcionalidades, no un centro a 3. Por ejemplo, muchas clases necesitan un asignador, un sistema de archivos, trabajar con cadenas, trabajar con flujos y acceso a bases de datos.
Cada estrategia / interfaz requerirá un miembro de tipo
std::shared_ptr<CreateStreamStrategy> m_create_stream;
. Dos mutadores, más inicialización en cada uno de los tres constructores. Además, en algún lugar de la inicialización de nuestra clase, deberá llamar a algo como
myclass->SetCreateStreamStrategy( my_create_stream_strategy )
par de veces, para un total de 8 unidades por interfaz / estrategia, y como tenemos alrededor de cinco, habrá 40 unidades. Es decir, hicimos la clase fuente dos veces más engorrosa. Y la pérdida de simplicidad afectará inevitablemente la legibilidad, y en algún otro lugar del proceso de depuración, y media veces, a pesar de que nada parece haber cambiado esencialmente.
Entonces la pregunta es. ¿Cómo hacer lo mismo, pero a un precio mínimo? Lo primero que viene a la mente es la parametrización estática en las plantillas, al estilo de Alexandrescu y la biblioteca Loki.
Estamos escribiendo una clase de estilo
template < struct Traits > class MyClass { public: void DoMainTaskFunction() { ... MyStream stream = Traits::streamwork::Open( stream_name ); ... } };
Esta decisión tiene todas las ventajas arquitectónicas que identificamos en la primera parte. Pero también hay muchas desventajas.
A mí mismo me encanta jugar, pero lamento por mí mismo, lo admito: las plantillas en el código ordinario son amadas solo por los magos de plantillas. Una gran cantidad de programadores con la palabra "plantilla" frunce el ceño ligeramente. Además, en la industria, la gran mayoría de las ventajas no son ventajas, sino que se vuelven a entrenar ligeramente en los syshniks de C ++ que no tienen un conocimiento profundo de las ventajas, pero caen bajo la palabra "plantilla" y fingen estar muertos.
Si traducimos esto a un lenguaje de producción, entonces mantener el código en la parametrización estática es más costoso y más complicado.
Al mismo tiempo, si queremos, con el propósito de una mayor legibilidad, eliminar cuidadosamente el cuerpo de la función fuera de la clase, obtendremos muchos garabatos con los nombres de plantillas y parámetros de plantilla. Y en caso de un error de compilación, obtenemos largos estantes de causas y áreas problemáticas legibles por humanos con un conjunto de plantillas anidadas complejas.
Pero, hay una salida simple. Como mago de plantilla, declaro que casi todo lo que se puede hacer usando la parametrización estática / polimorfismo estático se puede transferir al polimorfismo dinámico. No, por supuesto, no erradicaremos la plantilla del mal hasta el final, pero no la dispersaremos con una mano generosa para la parametrización en cada clase, sino que la limitaremos a un par de clases instrumentales.
Tercera parte La solución propuesta y el código codificado para esta solución.
¡¡¡¡AHÍ !!! Conozca la clase de plantilla TypedSet. Asocia un puntero inteligente de este tipo con un solo tipo. Además, para el tipo especificado puede tener un objeto, pero puede que no. No me gusta el nombre, por lo que agradeceré si en los comentarios me dicen una opción más exitosa.
Un tipo: un objeto. ¡Pero el número de tipos no está limitado! Por lo tanto, puede pasar una clase como un parametrizador.
Quiero llamar su atención sobre un punto. Puede parecer que en algún momento puede necesitar dos objetos en una interfaz. De hecho, si surge tal necesidad, entonces (en mi opinión) esto significa un error arquitectónico. Es decir, si tiene dos objetos bajo una interfaz, ya no son interfaces de acceso funcionales: estas son variables de entrada para la función, o no tiene una sino dos funcionalidades a las que necesita acceso, entonces es mejor dividir la interfaz en dos .
Realizaremos tres funciones básicas: Crear, Obtener y Has. En consecuencia, la creación, recepción y verificación de la presencia de un elemento.
Por cierto, vi una solución alternativa de colegas que escribían en Qt. Allí, el acceso a la interfaz deseada se realizó a través de un singleton, que "mapeó" la interfaz deseada, empaquetada en Varaint, a través de una línea de texto (!!!), y después de emitir esta opción, el resultado podría ser utilizado.
GlobalConfigurator()["FileSystem"].Get().As<FileSystem>()
Ciertamente funciona, pero la sobrecarga de contar la longitud y seguir cortando la cadena es algo aterrador para mi alma optimista. Aquí, la sobrecarga es cero, porque La elección de la interfaz deseada se realiza en tiempo de compilación.
Basado en TypedSet, podemos crear la clase StrategiesSet, que ya es más avanzada. En él almacenaremos no solo un objeto por interfaz de acceso para cada funcional, sino también para cada interfaz (en lo sucesivo, la estrategia) un TypedSet adicional con parámetros para esta estrategia. Aclaro: los parámetros, a diferencia de las variables de función, son los que se establecen una vez durante la inicialización del programa o una vez para ejecutar un programa grande. Los parámetros le permiten hacer que el código sea verdaderamente multiplataforma. Es en ellos que manejamos toda la cocina dependiente de la plataforma.
Aquí tendremos funciones más básicas: Create, Get, CreateParamsSet y GetParamsSet. No se ha establecido, porque es arquitectónicamente redundante: si su código se refiere a la funcionalidad de trabajar con el sistema de archivos, pero el código de llamada no lo proporcionó, solo puede lanzar una excepción o afirmar, o
hacer que el programa sebukka llame a la función 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>();
Una ventaja adicional es que, en la etapa de creación de prototipos, puede hacer una clase de mecanografía súper grande, acceder a todos los módulos y pasarla a todos los módulos como parámetro, volverse pequeña rápidamente y luego dividirla en partes que son mínimamente necesarias para cada módulo.
Bueno, y un caso de uso pequeño y (aún) demasiado simplificado. Espero que en los comentarios me sugiera lo que le gustaría ver como un ejemplo simple, y haré que el artículo sea una pequeña actualización. Como dice la popular sabiduría de programación, "Suelte lo antes posible y mejore el uso de comentarios después del lanzamiento".
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; }
Resumen
Por lo tanto, resolvimos un problema importante: dejamos en la clase solo la interfaz que está directamente relacionada con la funcionalidad de la clase. El resto se "introdujo" en el conjunto de estrategias, evitando al mismo tiempo abarrotar la clase con elementos innecesarios y "clavando" ciertas funcionalidades que necesitábamos para los algoritmos. Esto nos permitirá no solo escribir código altamente aislado, con cero dependencias en implementaciones y bibliotecas, sino también ahorrar una gran cantidad de tiempo.
El código para el ejemplo y las clases de herramientas se pueden encontrar
aquí.Upd. del 13/11/2019De hecho, el código que se muestra aquí es solo un ejemplo simplificado de legibilidad. El hecho es que typeid (). Hash_code se implementa en compiladores modernos de manera lenta e ineficiente. Su uso mata gran parte del significado. Además, como
sugirió el respetado
0xd34df00d , el estándar no garantiza la capacidad de distinguir tipos por código hash (en la práctica, este enfoque, sin embargo, funciona). Pero el ejemplo está bien leído. Reescribí TypedSet sin typeid (). Hash_code (), además, reemplacé map con array (pero con la capacidad de cambiar rápidamente de map a array y viceversa cambiando un dígito en #if). Resultó ser más difícil, pero más interesante para uso práctico.
en 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_; }; }
Aquí el acceso se lleva a cabo en tiempo lineal, los hashes de tipo se cuentan antes de que se ejecute main (), las pérdidas son solo para verificaciones de validación, que pueden desecharse si se desea.