variable que punteros puntero programacion funciones ejemplo declaracion cadenas c++ templates language-lawyer rtti

c++ - que - ¿Es seguro usar la dirección de una variable local estática dentro de una plantilla de función como un identificador de tipo?



que es un puntero en c++ (4)

Como lo menciona @StoryTeller , funciona bien en tiempo de ejecución .
Significa que no puedes usarlo como sigue:

template<int *> struct S {}; //... S<type_id<char>()> s;

Además, no es un identificador fijo. Por lo tanto, no tienes garantías de que el char esté vinculado al mismo valor a través de diferentes ejecuciones de tu ejecutable.

Si puedes lidiar con estas limitaciones, está bien.

Si ya conoce los tipos para los que desea un identificador persistente, puede usar algo como esto en su lugar (en C ++ 14):

template<typename T> struct wrapper { using type = T; constexpr wrapper(std::size_t N): N{N} {} const std::size_t N; }; template<typename... T> struct identifier: wrapper<T>... { template<std::size_t... I> constexpr identifier(std::index_sequence<I...>): wrapper<T>{I}... {} template<typename U> constexpr std::size_t get() const { return wrapper<U>::N; } }; template<typename... T> constexpr identifier<T...> ID = identifier<T...>{std::make_index_sequence<sizeof...(T)>{}};

Y crea sus identificadores como sigue:

constexpr auto id = ID<int, char>;

Puede usar esos identificadores más o menos como lo hizo con su otra solución:

handlers[id.get<T>()] = ...

Además, puede usarlos siempre que se requiera una expresión constante.
Como ejemplo como un parámetro de plantilla:

template<std::size_t> struct S {}; // ... S<id.get<B>()> s{};

En una declaración de cambio:

switch(value) { case id.get<char>(): // .... break; case id.get<int>(): // ... break; } }

Y así. Tenga en cuenta también que son persistentes a través de diferentes procesos siempre que no cambie la posición de un tipo en la lista de parámetros de plantilla de ID .

El principal inconveniente es que debe conocer todos los tipos para los que necesita un identificador cuando introduce la variable id .

Deseo crear una alternativa a std::type_index que no requiera RTTI :

template <typename T> int* type_id() { static int x; return &x; }

Tenga en cuenta que la dirección de la variable local x se usa como ID de tipo, no el valor de x mismo. Además, no pretendo usar un puntero desnudo en realidad. Acabo de eliminar todo lo que no es relevante para mi pregunta. Vea mi implementación real de type_index here .

¿Suena este enfoque, y si es así, por qué? ¿Si no, porque no? Siento que estoy en un terreno inestable aquí, por lo que estoy interesado en las razones precisas por las que mi enfoque funcionará o no.

Un caso de uso típico podría ser registrar rutinas en tiempo de ejecución para manejar objetos de diferentes tipos a través de una única interfaz:

class processor { public: template <typename T, typename Handler> void register_handler(Handler handler) { handlers[type_id<T>()] = [handler](void const* v) { handler(*static_cast<T const*>(v)); }; } template <typename T> void process(T const& t) { auto it = handlers.find(type_id<T>()); if (it != handlers.end()) { it->second(&t); } else { throw std::runtime_error("handler not registered"); } } private: std::map<int*, std::function<void (void const*)>> handlers; };

Esta clase podría usarse así:

processor p; p.register_handler<int>([](int const& i) { std::cout << "int: " << i << "/n"; }); p.register_handler<float>([](float const& f) { std::cout << "float: " << f << "/n"; }); try { p.process(42); p.process(3.14f); p.process(true); } catch (std::runtime_error& ex) { std::cout << "error: " << ex.what() << "/n"; }

Conclusión

Gracias a todos por vuestra ayuda. He aceptado la respuesta de @StoryTeller ya que él ha explicado por qué la solución debe ser válida según las reglas de C ++. Sin embargo, @SergeBallesta y algunos otros en los comentarios han señalado que MSVC realiza optimizaciones que se acercan incómodamente a romper este enfoque. Si se necesita un enfoque más sólido, entonces una solución que use std::atomic puede ser preferible, como sugiere @galinette:

std::atomic_size_t type_id_counter = 0; template <typename T> std::size_t type_id() { static std::size_t const x = type_id_counter++; return x; }

Si alguien tiene más pensamientos o información, ¡todavía estoy ansioso por escucharla!


Esto es coherente con el estándar porque C ++ usa plantillas y no genéricos con borrado de tipos como Java, por lo que cada tipo declarado tendrá su propia implementación de funciones que contiene una variable estática. Todas esas variables son diferentes y, como tales, deberían tener direcciones diferentes.

El problema es que su valor nunca se usa y lo peor nunca cambia. Recuerdo que los optimizadores pueden combinar constantes de cadena. Como los optimizadores hacen todo lo posible para ser mucho más inteligentes que cualquier programador humano, me temo que un compilador optimizador demasiado entusiasta descubra que como esos valores variables nunca cambian, todos mantendrán un valor de 0, ¿por qué no fusionarlos todos a guardar la memoria?

Sé que debido a la regla como si, el compilador es libre de hacer lo que quiera siempre que los resultados observables sean los mismos. Y no estoy seguro de que las direcciones de variables estáticas que siempre compartirán el mismo valor sean diferentes o no. ¿Tal vez alguien podría confirmar qué parte del estándar realmente se preocupa por él?

Los compiladores actuales aún compilan unidades de programa por separado, por lo que no pueden estar seguros de si otra unidad de programa usará o cambiará el valor. Entonces mi opinión es que el optimizador no tendrá suficiente información para decidir fusionar la variable, y su patrón es seguro.

Pero como realmente no creo que el estándar lo proteja, no puedo decir si las versiones futuras de constructores de C ++ (compilador + vinculador) no inventarán una fase de optimización global buscando activamente variables sin cambios que puedan fusionarse. Más o menos lo mismo que buscan activamente UB para optimizar partes del código ... Solo están protegidos los patrones comunes, donde no les permite romper una base de código demasiado grande, y no creo que el suyo sea lo suficientemente común.

Una forma un tanto estrafalaria de evitar que una fase de optimización combine variables que tengan el mismo valor sería simplemente darle a cada una un valor diferente:

int unique_val() { static int cur = 0; // normally useless but more readable return cur++; } template <typename T> void * type_id() { static int x = unique_val(); return &x; }

Ok, esto ni siquiera intenta ser seguro para subprocesos, pero no es un problema aquí: los valores nunca se usarán por sí mismos. Pero ahora tiene diferentes variables que tienen una duración estática (por 14.8.2 de estándar según lo dicho por @StoryTeller), que excepto en condiciones de carrera tienen valores diferentes. A medida que se usan, deben tener diferentes direcciones y usted debe estar protegido para mejorar en el futuro la optimización de los compiladores ...

Nota: creo que como el valor no se usará, devolver un void * suena más limpio ...

Solo una adición robada de un comentario de @bogdan. Se sabe que MSVC tiene una optimización muy agresiva con la bandera /OPT:ICF . La discusión sugiere que no debe ser conforme, y que solo se aplica a la variable marcada como const. Pero refuerza mi opinión de que incluso si el código de OP parece ser conforme, no me atrevería a usarlo sin precauciones adicionales en el código de producción.


Sí, será correcto hasta cierto punto. Las funciones de plantilla están implícitamente en inline , y los objetos estáticos en las funciones en inline se comparten en todas las unidades de traducción.

Entonces, en cada unidad de traducción, obtendrá la dirección de la misma variable local estática para la llamada a type_id<Type>() . Aquí está protegido de las infracciones de ODR según el estándar.

Por lo tanto, la dirección de la estática local se puede utilizar como un tipo de identificador de tipo de tiempo de ejecución elaborado en casa.


Edición posterior al comentario : al principio no me di cuenta de que la dirección se usaba como la clave, no el valor int. Es un enfoque ingenioso, pero en mi humilde opinión sufre un defecto importante: la intención no está clara si alguien más encuentra ese código.

Parece un viejo truco de C. Es inteligente, eficiente, pero el código no explica por sí mismo en absoluto cuál es la intención. Que en el moderno c ++, imho, es malo. Escribir código para programadores, no para compiladores. A menos que haya demostrado que hay un cuello de botella grave que requiere la optimización de metal desnudo.

Yo diría que debería funcionar, pero claramente no soy un abogado de idiomas ...

Una solución de constexpr elegante, pero compleja, se puede encontrar here o here

Respuesta original

Es "seguro" en el sentido de que es válido c ++ y puede acceder al puntero devuelto en todo su programa, ya que el local estático se inicializará en la primera llamada de función. Habrá una variable estática por tipo T utilizada en su código.

Pero :

  • ¿Por qué devolver un puntero no const? Esto permitirá a los llamantes cambiar el valor de la variable estática, que claramente no es algo que te gustaría
  • Si devuelvo un puntero const, no veo ningún interés en no devolver por valor en lugar de devolver el puntero

Además, este enfoque para obtener un ID de tipo solo funcionará en tiempo de compilación, no en tiempo de ejecución con objetos polimórficos. Por lo tanto, nunca devolverá el tipo de clase derivada de una referencia base o puntero.

¿Cómo inicializará los valores int estáticos? Aquí no los inicializa, así que esto no es válido. ¿Quizás quisiste usar el puntero no const para inicializarlos en alguna parte?

Hay dos mejores posibilidades:

1) especialice la plantilla para todos los tipos que desea apoyar

template <typename T> int type_id() { static const int id = typeInitCounter++; return id; } template <> int type_id<char>() { static const int id = 0; return id; //or : return 0 } template <> int type_id<unsigned int>() { static const int id = 1; return id; //or : return 1 } //etc...

2) Usa un contador global

std::atomic<int> typeInitCounter = 0; template <typename T> int type_id() { static const int id = typeInitCounter++; return id; }

Este último enfoque es en mi humilde opinión mejor porque no tienes que administrar tipos. Y como lo señala ASH, el contador incrementado basado en cero permite usar un vector lugar de un map que es mucho más simple y eficiente.

Además, use un map unordered_map lugar de un map para esto, no necesita ordenar. Esto le da O (1) acceso en lugar de O (log (n))