C ++ typedef fortemente tipizzato


50

Ho cercato di pensare a un modo per dichiarare dattiloscritti fortemente tipizzati, per catturare una certa classe di bug nella fase di compilazione. È spesso il caso in cui inserirò un int in diversi tipi di ID, o un vettore da posizionare 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 notte di programmazione si potrebbero fare errori sciocchi come confrontare diversi tipi di ID, o forse aggiungere una posizione a 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 i typedef fortemente tipizzati includono l'uso di boost, che almeno per me non è una possibilità (ho almeno c ++ 11). Quindi, dopo aver riflettuto un po ', ho avuto l'idea e volevo farla funzionare da qualcuno.

Innanzitutto, dichiari il tipo di base come modello. Il parametro template non viene utilizzato per nulla 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 degli amici devono in realtà essere dichiarate in avanti prima della definizione della classe, che richiede una dichiarazione diretta della classe modello.

Definiamo quindi tutti i membri per il tipo di base, ricordando solo che si tratta di una classe modello.

Alla fine, quando vogliamo usarlo, lo scriviamo come:

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

I tipi sono ora completamente separati. Le funzioni che accettano un EntityID generano un errore del compilatore se si tenta di alimentare loro un ModelID, ad esempio. Oltre a dover dichiarare i tipi di base come modelli, con i problemi che ciò 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 così liberamente come prima. Dove prima di moltiplicare un vettore per 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 fortemente tipizzato dovrei dire al compilatore che il multiplo di una Velocity per 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 esplicitamente ogni conversione, il che può essere una seccatura. D'altra parte, questa limitazione può aiutare a prevenire altri tipi di errori (diciamo, forse, moltiplicare una velocità per una distanza, che non avrebbe senso in questo dominio). Quindi sono lacerato e mi chiedo se le persone abbiano opinioni sul mio problema originale o sul mio approccio per risolverlo.



Risposte:


40

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

E parlando di spazi, questa è un'utile applicazione di tipi 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, tuttavia, 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 stiamo usando i valori fantasma per taggare i valori di runtime con informazioni in fase di compilazione sugli esponenti sulle unità coinvolte. Questo si adatta meglio rispetto alla creazione di strutture separate per velocità, distanze e così via, e potrebbe essere sufficiente per coprire il tuo caso d'uso.


2
Hmm, usare il sistema di template per far rispettare le unità durante le operazioni è fantastico. Non ci avevo pensato, grazie! Ora mi chiedo se puoi applicare cose come le conversioni tra metro e chilometro, per esempio.
Kian,

@Kian: Presumibilmente useresti le unità base SI internamente — m, kg, s, A, & c. — e definiresti semplicemente un alias 1km = 1000m per comodità.
Jon Purdy,

7

Ho avuto un caso simile in cui volevo distinguere significati diversi di alcuni valori interi e proibire 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 il Tcostruttore explicit. Il Meaningviene quindi utilizzato in questo modo:

typedef Explicit<int, struct EntityIDTag> EntityID;
typedef Explicit<int, struct ModelIDTag> ModelID;

1
Questo è interessante, ma non sono sicuro che sia abbastanza forte. Garantirà che se dichiaro una funzione con il tipo typedefed, solo gli elementi giusti possono essere usati come parametri, il che è positivo. Ma per ogni altro uso aggiunge un sovraccarico sintattico senza impedire la miscelazione dei parametri. Pronuncia operazioni come il confronto. operator == (int, int) prenderà un EntityID e un ModelID senza lamentele (anche se esplicito richiede che lo abbia lanciato, non mi impedisce di usare le variabili sbagliate).
Kian,

Sì. Nel mio caso ho dovuto impedirmi di assegnare diversi tipi di ID l'uno all'altro. I confronti e le operazioni aritmetiche non erano la mia principale preoccupazione. Il costrutto sopra vieterà l'assegnazione, ma non altre operazioni.
Mindriot

Se sei disposto a mettere più energia in questo, puoi costruire una versione (abbastanza) generica che gestisca anche gli operatori, rendendo la classe Explicit avvolgente gli operatori più comuni. Vedere pastebin.com/FQDuAXdu per un esempio: sono necessari alcuni costrutti SFINAE abbastanza complessi per determinare se la classe wrapper fornisce effettivamente gli operatori wrapper (vedere questa domanda SO ). Intendiamoci, non può ancora coprire tutti i casi e potrebbe non valere la pena.
Mindriot,

Sebbene sintatticamente elegante, questa soluzione comporterà una significativa penalizzazione delle prestazioni per i tipi interi. I numeri interi possono essere passati tramite registri, le strutture (anche contenenti un solo numero intero) non possono.
Ghostrider,

1

Non sono sicuro di come funzioni nel codice di produzione (sono un principiante C ++ / programmatore, come CS101 principiante), ma ho preparato questo usando il macro sys di 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; } }

Nota: per favore fatemi sapere di eventuali insidie ​​/ miglioramenti che pensate.
Noein,

1
Puoi aggiungere del codice che mostra come viene utilizzata questa macro, come negli esempi nella domanda originale? Se è così, questa è un'ottima risposta.
Jay Elston,
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.