C ++ typedef strongmente tipizzato

44

Ho cercato di pensare a un modo di dichiarare typedef strongmente tipizzati, per catturare una certa classe di bug nella fase di compilazione. Spesso succede che digiterò un int in diversi tipi di ID o un vettore in posizione o velocità:

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

Questo può rendere più chiaro l'intento del codice, ma dopo una lunga nottata di codifica si potrebbero commettere errori stupidi come confrontare diversi tipi di ID o aggiungere una posizione ad una velocità.

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

Sfortunatamente, i suggerimenti che ho trovato per typedef strongmente tipizzati includono l'uso di boost, che almeno per me non è una possibilità (ho almeno c ++ 11). Quindi, dopo un po 'di riflessione, mi sono imbattuto in questa idea, e volevo gestirla da qualcuno.

Per prima cosa dichiari il tipo di base come modello. Il parametro template non viene utilizzato per qualcosa nella definizione, tuttavia:

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 );
};

Le funzioni di amicizia devono essere inoltrate prima della definizione della classe, che richiede una dichiarazione in avanti della classe template.

Quindi definiamo tutti i membri per il tipo base, ricordando semplicemente che si tratta di una classe template.

Infine, quando vogliamo usarlo, lo digitiamo come:

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

I tipi ora sono completamente separati. Le funzioni che richiedono un EntityID genereranno un errore del compilatore se si tenta di fornire loro un ModelID, ad esempio. Oltre a dover dichiarare i tipi di base come modelli, con i problemi che comporta, è anche abbastanza compatto.

Speravo che qualcuno avesse commenti o critiche su questa idea?

Un problema che mi è venuto in mente mentre scrivevo questo, nel caso di posizioni e velocità, per esempio, sarebbe che non posso convertire tra i tipi liberamente come prima. Dove prima moltiplicare un vettore con uno scalare darebbe un altro vettore, quindi potrei fare:

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 il mio typedef strongmente tipizzato dovrei dire al compilatore che multypling a Velocity by a Time risulta in una posizione.

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

Per risolvere questo, penso che dovrei specializzare ogni conversione in modo esplicito, il che può essere un po 'fastidioso. D'altra parte, questa limitazione può aiutare a prevenire altri tipi di errori (ad esempio, moltiplicando una Velocity by a Distance, forse, che non avrebbe senso in questo dominio). Quindi sono distrutto e mi chiedo se le persone hanno qualche opinione sul mio problema originale o sul mio approccio alla risoluzione.

    
posta Kian 05.06.2014 - 23:26
fonte

3 risposte

34

Questi sono parametri del tipo di fantasma , cioè i parametri di un tipo parametrico che non sono usati per la loro rappresentazione, ma per separare diversi "spazi" di tipi con la stessa rappresentazione.

E parlando di spazi, questa è un'applicazione utile dei tipi di 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) { … }

Come hai visto, però, ci sono alcune difficoltà con i tipi di unità. Una cosa che puoi fare è scomporre le unità in un vettore di esponenti interi sui componenti fondamentali:

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;

Qui utilizziamo valori fantasma per contrassegnare i valori di runtime con informazioni in fase di compilazione sugli esponenti delle unità coinvolte. Questa scala è migliore delle strutture separate per velocità, distanze e così via, e potrebbe essere sufficiente per coprire il tuo caso d'uso.

    
risposta data 06.06.2014 - 01:51
fonte
6

Ho avuto un caso simile in cui volevo distinguere diversi significati di alcuni valori interi e proibire le conversioni implicite tra di loro. Ho scritto una classe generica come questa:

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;
};

Ovviamente, se vuoi essere ancora più sicuro, puoi anche creare T costruttore explicit . Il Meaning viene quindi utilizzato in questo modo:

typedef Explicit<int, struct EntityIDTag> EntityID;
typedef Explicit<int, struct ModelIDTag> ModelID;
    
risposta data 06.06.2014 - 14:04
fonte
1

Non sono sicuro di come funzioni il seguente codice di produzione (sono un principiante in programmazione C ++, come, principiante CS101), ma l'ho cucinato usando i macro sys del 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; } }
    
risposta data 20.05.2015 - 10:05
fonte

Leggi altre domande sui tag