C ++ stark typisierte typedef

49

Ich habe versucht, eine Möglichkeit zu finden, stark typisierte typedefs zu deklarieren, um eine bestimmte Klasse von Fehlern in der Kompilierungsphase zu erkennen. Es kommt häufig vor, dass ich ein int in mehrere Arten von ids oder einen Vektor zur Positionierung oder Geschwindigkeit eingebe:

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

Dies kann die Absicht des Codes klarer machen, aber nach einer langen Nacht des Codierens kann es zu dummen Fehlern kommen, wie dem Vergleichen verschiedener Arten von IDs oder dem Hinzufügen einer Position zu einer Geschwindigkeit.

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

Leider habe ich Vorschläge für stark typisierte Typedefs gefunden, die die Verwendung von Boost beinhalten, was zumindest für mich keine Möglichkeit ist (ich habe zumindest C ++ 11). Nach einigem Überlegen bin ich auf diese Idee gekommen und wollte sie von jemandem ausführen lassen.

Zunächst deklarieren Sie den Basistyp als Vorlage. Der template-Parameter wird jedoch für nichts in der Definition verwendet:

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

Friend-Funktionen müssen tatsächlich vor der Klassendefinition als Forward deklariert werden, was eine Forward-Deklaration der Template-Klasse erfordert.

Anschließend definieren wir alle Elemente für den Basistyp, wobei wir uns nur daran erinnern, dass es sich um eine Vorlagenklasse handelt.

Schließlich, wenn wir es verwenden möchten, tippen wir es als:

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

Die Typen sind jetzt völlig getrennt. Funktionen, die eine EntityID annehmen, lösen einen Compilerfehler aus, wenn Sie versuchen, ihnen stattdessen beispielsweise eine ModelID zuzuweisen. Abgesehen davon, dass die Basistypen als Vorlagen deklariert werden müssen, ist sie mit den damit verbundenen Problemen auch recht kompakt.

Ich hatte gehofft, jemand hätte Kommentare oder Kritik zu dieser Idee?

Ein Problem, das mir beim Schreiben in den Sinn kam, zum Beispiel bei Positionen und Geschwindigkeiten, war, dass ich nicht so frei zwischen den Typen konvertieren kann wie zuvor. Wo vor dem Multiplizieren eines Vektors mit einem Skalar ein anderer Vektor erhalten würde, könnte ich Folgendes tun:

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;

Mit meinem stark typisierten typedef müsste ich dem Compiler mitteilen, dass das Multiplizieren einer Velocity mit einer Time zu einer Position führt.

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

Um dies zu lösen, müsste ich jede Konvertierung explizit spezialisieren, was eine Art Mühe sein kann. Andererseits kann diese Einschränkung helfen, andere Arten von Fehlern zu vermeiden (z. B. das Multiplizieren einer Geschwindigkeit mit einer Entfernung, was in diesem Bereich möglicherweise keinen Sinn ergibt). Also bin ich hin und her gerissen und frage mich, ob die Leute eine Meinung zu meinem ursprünglichen Problem oder meinem Lösungsansatz haben.

Kian
quelle
Schauen Sie sich das an: zumalifeguard.wikia.com/wiki/Idtypes.idl
zumalifeguard 06.06.14
Die gleiche Frage ist hier: stackoverflow.com/q/23726038/476681
BЈовић

Antworten:

39

Dies sind Phantomtyp-Parameter , dh Parameter eines parametrisierten Typs, die nicht für ihre Darstellung verwendet werden, sondern zum Trennen verschiedener „Räume“ von Typen mit derselben Darstellung.

Apropos Räume, das ist eine nützliche Anwendung für Phantomtypen:

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

Wie Sie gesehen haben, gibt es jedoch einige Schwierigkeiten mit den Einheitentypen. Eine Sache, die Sie tun können, ist die Zerlegung von Einheiten in einen Vektor ganzzahliger Exponenten für die Grundkomponenten:

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;

Hier verwenden wir Phantomwerte, um Laufzeitwerte mit Informationen zur Kompilierungszeit über die Exponenten der beteiligten Einheiten zu kennzeichnen. Dies ist besser skalierbar als das Erstellen separater Strukturen für Geschwindigkeiten, Abstände usw. und könnte ausreichen, um Ihren Anwendungsfall abzudecken.

Jon Purdy
quelle
2
Hmm, die Verwendung des Vorlagensystems zur Durchsetzung von Einheiten bei Operationen ist cool. Hatte nicht daran gedacht, danke! Jetzt frage ich mich, ob Sie beispielsweise Umrechnungen zwischen Metern und Kilometern erzwingen können.
Kian
@Kian: Vermutlich würden Sie die SI-Basiseinheiten intern verwenden - m, kg, s, A usw. - und der Einfachheit halber nur einen Alias ​​1 km = 1000 m definieren.
Jon Purdy
7

Ich hatte einen ähnlichen Fall, in dem ich verschiedene Bedeutungen einiger ganzzahliger Werte unterscheiden und implizite Konvertierungen zwischen ihnen verbieten wollte. Ich habe eine generische Klasse wie diese geschrieben:

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

Wenn Sie noch sicherer sein möchten, können Sie natürlich auch den TKonstruktor erstellen explicit. Das Meaningwird dann so verwendet:

typedef Explicit<int, struct EntityIDTag> EntityID;
typedef Explicit<int, struct ModelIDTag> ModelID;
Mindriot
quelle
1
Das ist interessant, aber ich bin mir nicht sicher, ob es stark genug ist. Es wird sichergestellt, dass, wenn ich eine Funktion mit dem Typ typedefed deklariere, nur die richtigen Elemente als Parameter verwendet werden können, was gut ist. Bei jeder anderen Verwendung wird der syntaktische Aufwand erhöht, ohne dass das Mischen von Parametern verhindert wird. Sagen Sie Operationen wie Vergleichen. operator == (int, int) nimmt eine EntityID und eine ModelID ohne Beanstandung (auch wenn explizit verlangt wird, dass ich sie umsetze, hindert es mich nicht daran, die falschen Variablen zu verwenden).
Kian
Ja. In meinem Fall musste ich mich davon abhalten, verschiedene Arten von IDs einander zuzuweisen. Vergleiche und Rechenoperationen waren nicht mein Hauptanliegen. Das obige Konstrukt verbietet die Zuweisung, aber keine anderen Operationen.
Mindriot
Wenn Sie bereit sind, mehr Energie in dieses zu stecken, können Sie eine (ziemlich) generische Version erstellen, die auch Operatoren behandelt, indem Sie den Explicit-Klassenumbruch zu den gebräuchlichsten Operatoren machen. Siehe pastebin.com/FQDuAXdu für ein Beispiel - Sie einige ziemlich komplexe SFINAE Konstrukte müssen , um zu bestimmen , ob die Wrapper - Klasse die verpackten Betreiber tatsächlich bietet oder nicht (siehe diese Frage SO ). Wohlgemerkt, es kann immer noch nicht alle Fälle abdecken und ist die Mühe möglicherweise nicht wert.
Mindriot
Obwohl diese Lösung syntaktisch elegant ist, ist sie für Integer-Typen mit erheblichen Leistungseinbußen verbunden. Ganzzahlen können über Register übergeben werden, Strukturen (auch mit einer einzelnen Ganzzahl) nicht.
Ghostrider
1

Ich bin nicht sicher, wie das Folgende im Produktionscode funktioniert (ich bin ein Anfänger in C ++ / Programmierung, wie CS101-Anfänger), aber ich habe dies mit C ++ 's Makrosys erfunden.

#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
quelle
Hinweis: Bitte lassen Sie mich wissen, welche Fallstricke / Verbesserungen Ihnen einfallen.
Noein
1
Können Sie Code hinzufügen, der zeigt, wie dieses Makro verwendet wird - wie in den Beispielen in der ursprünglichen Frage? Wenn ja, ist dies eine gute Antwort.
Jay Elston