C ++ typedef tipado fuertemente

44

He estado tratando de pensar en una forma de declarar typedefs tipográficamente, para detectar una cierta clase de errores en la etapa de compilación. A menudo es el caso que tipearé un 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 ID 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 tipadas incluyen el uso de boost, que al menos para mí no es una posibilidad (al menos tengo c ++ 11). Así que, después de pensar un poco, se me ocurrió esta idea y quería que alguien la pusiera en marcha.

Primero, declara 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 );
};

Las funciones de amigo en realidad deben ser declaradas hacia delante 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 de compilador si intenta darles un ModelID en su lugar, 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 me vino a la mente al escribir esto, en el caso de las posiciones y las velocidades, por ejemplo, es que no puedo convertir los tipos con tanta libertad como antes. Donde antes, multiplicar un vector por un escalar daría otro vector, así 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 el hecho de que unir un Velocity by a Time resulte en 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 explícitamente cada conversión, lo que puede ser una molestia. Por otro lado, esta limitación puede ayudar a prevenir otros tipos de errores (por ejemplo, multiplicar una velocidad por una distancia, tal vez, lo que no tendría sentido en este dominio). Así que estoy desgarrado y me pregunto si la gente tiene alguna opinión sobre mi problema original o mi enfoque para resolverlo.

    
pregunta Kian 05.06.2014 - 23:26

3 respuestas

34

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, es una aplicación útil de tipos fantasma:

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 algunos problemas 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 utilizando valores fantasma para etiquetar los valores de tiempo de ejecución con información en tiempo de compilación sobre los exponentes en las unidades involucradas. Esto se escala mejor que haciendo estructuras separadas para velocidades, distancias, etc., y podría ser suficiente para cubrir su caso de uso.

    
respondido por el Jon Purdy 06.06.2014 - 01:51
6

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 T constructor explicit . El Meaning se usa así:

typedef Explicit<int, struct EntityIDTag> EntityID;
typedef Explicit<int, struct ModelIDTag> ModelID;
    
respondido por el mindriot 06.06.2014 - 14:04
1

No estoy seguro de cómo funciona lo siguiente en el código de producción (soy un principiante en programación en C ++, como, principiante en CS101), pero cociné esto utilizando los sistemas 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; } }
    
respondido por el Noein 20.05.2015 - 10:05

Lea otras preguntas en las etiquetas