C ++ fuertemente tipeado typedef

50

He estado tratando de pensar en una forma de declarar typedefs fuertemente tipados, para detectar una cierta clase de errores en la etapa de compilación. A menudo es el caso que escribo def de int en varios tipos de identificadores, o un vector para posicionar o velocidad:

typedef int EntityID;
typedef int ModelID;
typedef Vector3 Position;
typedef Vector3 Velocity;

Esto puede hacer que la intención del código sea más clara, pero después de una larga noche de codificación, uno podría cometer errores tontos como comparar diferentes tipos de identificadores, o agregar una posición a una velocidad tal vez.

EntityID eID;
ModelID mID;

if ( eID == mID ) // <- Compiler sees nothing wrong
{ /*bug*/ }


Position p;
Velocity v;

Position newP = p + v; // bug, meant p + v*s but compiler sees nothing wrong

Desafortunadamente, las sugerencias que he encontrado para typedefs fuertemente tipados incluyen el uso de boost, que al menos para mí no es una posibilidad (tengo al menos c ++ 11). Entonces, después de pensar un poco, me encontré con esta idea y quise ponerla en práctica por alguien.

Primero, declaras el tipo base como una plantilla. Sin embargo, el parámetro de plantilla no se usa para nada en la definición:

template < typename T >
class IDType
{
    unsigned int m_id;

    public:
        IDType( unsigned int const& i_id ): m_id {i_id} {};
        friend bool operator==<T>( IDType<T> const& i_lhs, IDType<T> const& i_rhs );
};

En realidad, las funciones de amigo deben declararse hacia adelante antes de la definición de clase, lo que requiere una declaración hacia adelante de la clase de plantilla.

Luego definimos todos los miembros para el tipo base, solo recordando que es una clase de plantilla.

Finalmente, cuando queremos usarlo, lo escribimos como:

class EntityT;
typedef IDType<EntityT> EntityID;
class ModelT;
typedef IDType<ModelT> ModelID;

Los tipos ahora están completamente separados. Las funciones que toman un EntityID arrojarán un error del compilador si intenta alimentarlos con un ModelID, por ejemplo. Además de tener que declarar los tipos base como plantillas, con los problemas que esto conlleva, también es bastante compacto.

¿Esperaba que alguien tuviera comentarios o críticas sobre esta idea?

Un problema que se me ocurrió al escribir esto, en el caso de las posiciones y las velocidades, por ejemplo, sería que no puedo convertir entre tipos tan libremente como antes. Donde antes de multiplicar un vector por un escalar daría otro vector, por lo que podría hacer:

typedef float Time;
typedef Vector3 Position;
typedef Vector3 Velocity;

Time t = 1.0f;
Position p = { 0.0f };
Velocity v = { 1.0f, 0.0f, 0.0f };

Position newP = p + v*t;

Con mi typedef fuertemente tipado, tendría que decirle al compilador que multiplicar una Velocidad por un Tiempo da como resultado una Posición.

class TimeT;
typedef Float<TimeT> Time;
class PositionT;
typedef Vector3<PositionT> Position;
class VelocityT;
typedef Vector3<VelocityT> Velocity;

Time t = 1.0f;
Position p = { 0.0f };
Velocity v = { 1.0f, 0.0f, 0.0f };

Position newP = p + v*t; // Compiler error

Para resolver esto, creo que tendría que especializar cada conversión explícitamente, lo que puede ser una molestia. Por otro lado, esta limitación puede ayudar a prevenir otros tipos de errores (por ejemplo, multiplicando una Velocidad por una Distancia, tal vez, lo que no tendría sentido en este dominio). Así que estoy desgarrado y me pregunto si las personas tienen alguna opinión sobre mi problema original o mi enfoque para resolverlo.

Kian
fuente
Echa un vistazo a esto: zumalifeguard.wikia.com/wiki/Idtypes.idl
zumalifeguard
la misma pregunta está aquí: stackoverflow.com/q/23726038/476681
B 6овић

Respuestas:

40

Estos son parámetros de tipo fantasma , es decir, parámetros de un tipo parametrizado que no se utilizan para su representación, sino para separar diferentes "espacios" de tipos con la misma representación.

Y hablando de espacios, esa es una aplicación útil de los tipos fantasmas:

template<typename Space>
struct Point { double x, y; };

struct WorldSpace;
struct ScreenSpace;

// Conversions between coordinate spaces are explicit.
Point<ScreenSpace> project(Point<WorldSpace> p, const Camera& c) {  }

Sin embargo, como has visto, hay algunas dificultades con los tipos de unidades. Una cosa que puede hacer es descomponer unidades en un vector de exponentes enteros en los componentes fundamentales:

template<typename T, int Meters, int Seconds>
struct Unit {
  Unit(const T& value) : value(value) {}
  T value;
};

template<typename T, int MA, int MB, int SA, int SB>
Unit<T, MA - MB, SA - SB>
operator/(const Unit<T, MA, SA>& a, const Unit<T, MB, SB>& b) {
  return a.value / b.value;
}

Unit<double, 0, 0> one(1);
Unit<double, 1, 0> one_meter(1);
Unit<double, 0, 1> one_second(1);

// Unit<double, 1, -1>
auto one_meter_per_second = one_meter / one_second;

Aquí estamos usando valores fantasmas para etiquetar valores de tiempo de ejecución con información en tiempo de compilación sobre los exponentes en las unidades involucradas. Esto escala mejor que hacer estructuras separadas para velocidades, distancias, etc., y podría ser suficiente para cubrir su caso de uso.

Jon Purdy
fuente
2
Hmm, usar el sistema de plantillas para imponer unidades en las operaciones es genial. No lo había pensado, gracias! Ahora me pregunto si puede hacer cumplir cosas como conversiones entre metro y kilómetro, por ejemplo.
Kian
@Kian: Presumiblemente usaría las unidades base del SI internamente (m, kg, s, A, etc.) y simplemente definiría un alias de 1 km = 1000 m para mayor comodidad.
Jon Purdy
7

Tuve un caso similar en el que quería distinguir diferentes significados de algunos valores enteros y prohibir las conversiones implícitas entre ellos. Escribí una clase genérica como esta:

template <typename T, typename Meaning>
struct Explicit
{
  //! Default constructor does not initialize the value.
  Explicit()
  { }

  //! Construction from a fundamental value.
  Explicit(T value)
    : value(value)
  { }

  //! Implicit conversion back to the fundamental data type.
  inline operator T () const { return value; }

  //! The actual fundamental value.
  T value;
};

Por supuesto, si quieres estar aún más seguro, también puedes hacer el Tconstructor explicit. La Meaningcontinuación, se utiliza la siguiente manera:

typedef Explicit<int, struct EntityIDTag> EntityID;
typedef Explicit<int, struct ModelIDTag> ModelID;
mindriot
fuente
1
Esto es interesante, pero no estoy seguro de que sea lo suficientemente fuerte. Asegurará que si declaro una función con el tipo typedefed, solo se pueden usar los elementos correctos como parámetros, lo cual es bueno. Pero para cualquier otro uso, agrega una sobrecarga sintáctica sin evitar la mezcla de parámetros. Decir operaciones como comparar. operator == (int, int) tomará un EntityID y un ModelID sin queja (aunque explícito requiera que lo eche, no me impide usar las variables incorrectas).
Kian
Si. En mi caso, tuve que evitar asignarme diferentes tipos de ID entre sí. Las comparaciones y las operaciones aritméticas no fueron mi principal preocupación. La construcción anterior prohibirá la asignación, pero no otras operaciones.
mindriot
Si está dispuesto a poner más energía en esto, puede construir una versión (bastante) genérica que también maneje operadores, haciendo que la clase Explícita abarque los operadores más comunes. Consulte pastebin.com/FQDuAXdu para ver un ejemplo: necesita algunas construcciones SFINAE bastante complejas para determinar si la clase wrapper realmente proporciona los operadores envueltos o no (vea esta pregunta SO ). Eso sí, todavía no puede cubrir todos los casos y puede que no valga la pena.
mindriot
Si bien es sintácticamente elegante, esta solución incurrirá en una penalización de rendimiento significativa para los tipos enteros. Los enteros se pueden pasar a través de registros, las estructuras (incluso si contienen un solo entero) no.
Ghostrider
1

No estoy seguro de cómo funciona lo siguiente en el código de producción (soy un principiante en C ++ / programación, como el principiante CS101), pero preparé esto usando el sistema macro de C ++.

#define newtype(type_, type_alias) struct type_alias { \

/* make a new struct type with one value field
of a specified type (could be another struct with appropriate `=` operator*/

    type_ inner_public_field_thing; \  // the masked_value
    \
    explicit type_alias( type_ new_value ) { \  // the casting through a constructor
    // not sure how this'll work when casting non-const values
    // (like `type_alias(variable)` as opposed to `type_alias(bare_value)`
        inner_public_field_thing = new_value; } }
Noein
fuente
Nota: Por favor, avíseme de cualquier dificultad / mejora que piense.
Noein
1
¿Puede agregar algún código que muestre cómo se usa esta macro, como en los ejemplos de la pregunta original? Si es así, esta es una gran respuesta.
Jay Elston