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.