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.
quelle
Antworten:
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:
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:
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.
quelle
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:
Wenn Sie noch sicherer sein möchten, können Sie natürlich auch den
T
Konstruktor erstellenexplicit
. DasMeaning
wird dann so verwendet:quelle
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.
quelle