code review c++ – ID de tipo de tiempo de compilación único (constexpr) sin RTTI

Pregunta:

Estoy creando una biblioteca en la que necesito generar una identificación única para un tipo donde la identificación debe conocerse en el momento de la compilación. Primero me basé en la dirección de una función de plantilla, pero resultó poco confiable tanto con MSVC como con Clang en Windows.

Luego se me ocurrió la dirección de un miembro de datos estáticos dentro de una clase de plantilla.

Quiero admitir la mayoría de los casos de uso posible, esto es lo que me viene a la mente:

  • Optimizaciones ávidas que fusionarían la definición de variables estáticas o funciones en línea
  • DLL y bibliotecas compartidas
  • Optimizaciones de tiempo de enlace

Mis pruebas fueron exitosas, pero no fue una configuración exótica con dlopen múltiples niveles o cosas así. No soy un experto en el tipo de caso de uso que quiero respaldar, por lo que es difícil saber si puedo asegurar que mis afirmaciones estén bien.

Mi diseño funciona declarando un miembro de datos estáticos de tipo T* dentro de una clase de plantilla, luego tomando su dirección como el valor de la ID.

namespace detail {

/**
 * Template class that hold the declaration of the id.
 * 
 * We use the pointer of this id as type id.
 */
template<typename T>
struct type_id_ptr {
    // Having a static data member will ensure (I hope) that it has only one address for the whole program.
    // Furthermore, the static data member having different types will ensure (I hope) it won't get optimized.
    static const T* const id;
};

/**
 * Definition of the id.
 */
template<typename T>
const T* const type_id_ptr<T>::id = nullptr;

} // namespace detail

/**
 * The type of a type id.
 */
using type_id_t = const void*;

/**
 * The function that returns the type id.
 * 
 * It uses the pointer to the static data member of a class template to achieve this.
 * Altough the value is not predictible, it's stable (I hope).
 */
template <typename T>
constexpr auto type_id() noexcept -> type_id_t {
    return &detail::type_id_ptr<T>::id;
}

¿Hay alguna advertencia a tener en cuenta con este diseño?


Aquí hay algunos usos de esto y motivación para este código:

constexpr auto id_of_int_type = type_id_t{type_id<int>()};
constexpr auto id_of_float_type = type_id_t{type_id<float>()};

static_assert(id_of_int_type != id_of_float_type);

Esto se puede compilar con -fno-rtti .

Además, se puede utilizar completamente en tiempo de compilación. Aquí está el equivalente con RTTI:

constexpr auto test = typeid(int); // won't compile 

Este código no se compilará ya que typeid no se puede usar en tiempo de compilación. Además, deshabilitar RTTI también deshabilita RTTI estático.

El código es útil porque se puede utilizar en la estructura de datos o para combinar con el tipo de borrado en tiempo de compilación.

std::map<type_id_t, void*> anything;

// sorry for raw new
anything[type_id<std::string>()] = new std::string{"hello"};


// compile time example
static auto static_int = int{};
static auto static_double = double{};

// type_id as the keys in a compile time map
constexpr auto anything_compiletime = frozen::unordered_map<type_id_t, void*, 2>{
    {type_id<int>(), &static_int},
    {type_id<double>(), &static_double}
};

Respuesta:

Estoy convencido de que tu diseño funcionará correctamente. La unicidad de las variables estáticas está garantizada y no se optimizarán si las define y las usa odr (tomar la dirección de una variable es usarla como odr). Sin embargo, ejecutarlo a través del desbordamiento de pila sería una buena idea, porque probablemente llamará la atención de uno de esos gurú de reputación de más de 200K que responde a todas las preguntas complicadas del lenguaje.

Lo que no puedo entender es lo que estás tratando de lograr con esto. La única propiedad útil de esta id tiempo de compilación es su singularidad, por lo que no logra nada más que el tipo en sí: ¿cuál es la diferencia entre if (type_id<A>() == type_id<B>()) y if (std::is_same_v<A, B>) ? Yo diría que logra mucho menos, porque los tipos tienen otros rasgos útiles (para una lista parcial pero ya preparada, solo eche un vistazo al encabezado type_traits en la biblioteca estándar).

Además, su generador de id no es realmente genérico: type_id<int&>() no se compilará porque un puntero no puede apuntar a una referencia (por lo tanto, static const int&* const id es ilegal). Por supuesto, siempre existe la posibilidad de eliminar la referencia, pero significa que usará type_trait s para que su type_id funcione, por lo que subraya que debe usar rasgos de tipo estándar bien establecidos en lugar de este nuevo type_id tiempo de type_id .

Sugeriría, si es posible, que publique aquí una parte más grande de su código, que contenga casos de uso, y deje que nuestro cerebro colectivo trabaje en la solución del problema más amplio.


Editar: ahora entiendo mejor lo que quieres hacer. Realmente me gusta la simplicidad de uso, y el truco me parece muy inteligente. En realidad, si fuera más feo, limitaciones como no permitir referencias no importarían mucho, porque todos lo verían como una solución alternativa. Pero dado que parece tan fluido, rápidamente podría volverse omnipresente en el código; entonces las limitaciones importan, sobre todo cuando dan lugar a oscuros mensajes de error.

De todos modos, creo que se me hubiera ocurrido un diseño menos ambicioso, que también sería más fácil de mantener y comprender (esta implementación concreta se basa en C ++ 17 pero no sería difícil de implementar en estándares anteriores):

#include <iostream>
#include <vector>

template <typename... Types>
struct Type_register{};

template <typename Queried_type>
constexpr int type_id(Type_register<>) { static_assert(false, "You shan't query a type you didn't register first"); return -1; }

template <typename Queried_type, typename Type, typename... Types>
constexpr int type_id(Type_register<Type, Types...>) {
    if constexpr (std::is_same_v<Type, Queried_type>) return 0;
    else return 1 + type_id<Queried_type>(Type_register<Types...>());
}

int main() {
   Type_register<int, float, char, std::vector<int>, int&> registered_types;
   constexpr auto test1 = type_id<int&>(registered_types);
   constexpr auto test2 = type_id<int*>(registered_types);
}

Deja un comentario

Tu dirección de correo electrónico no será publicada. Los campos obligatorios están marcados con *

Ir arriba