Próbowałem wymyślić sposób na zadeklarowanie silnie typowanych typów maszynopisów, aby złapać pewną klasę błędów na etapie kompilacji. Często zdarza się, że wpisuję int w kilku typach id lub wektorze do pozycji lub prędkości:
typedef int EntityID;
typedef int ModelID;
typedef Vector3 Position;
typedef Vector3 Velocity;
Może to uczynić cel kodu bardziej wyraźnym, ale po długiej nocy kodowania można popełnić głupie błędy, takie jak porównywanie różnych rodzajów identyfikatorów lub może dodanie pozycji do prędkości.
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
Niestety, sugestie, które znalazłem dla silnie typowanych typów typefów, obejmują użycie boosta, co przynajmniej dla mnie nie jest możliwe (mam przynajmniej c ++ 11). Po krótkim namyśle wpadłem na ten pomysł i chciałem go uruchomić.
Najpierw zadeklarujesz typ podstawowy jako szablon. Jednak parametr szablonu nie jest używany do niczego w definicji, ale:
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 );
};
Funkcje zaprzyjaźnione faktycznie muszą być zadeklarowane w przód przed definicją klasy, co wymaga deklaracji w przód klasy szablonu.
Następnie definiujemy wszystkich członków dla typu podstawowego, pamiętając tylko, że jest to klasa szablonów.
Wreszcie, gdy chcemy go użyć, wpisaliśmy go jako:
class EntityT;
typedef IDType<EntityT> EntityID;
class ModelT;
typedef IDType<ModelT> ModelID;
Typy są teraz całkowicie oddzielne. Funkcje pobierające identyfikator EntityID wyrzucą błąd kompilatora, jeśli spróbujesz na przykład podać im identyfikator modelu. Oprócz konieczności zadeklarowania typów podstawowych jako szablonów, z towarzyszącymi im problemami, jest również dość kompaktowy.
Miałem nadzieję, że ktoś miał komentarze lub krytykę na temat tego pomysłu?
Jednym z problemów, które przyszło mi do głowy podczas pisania tego, na przykład w przypadku pozycji i prędkości, jest to, że nie mogę swobodnie konwertować między typami tak swobodnie, jak wcześniej. Gdzie przed pomnożeniem wektora przez skalar dałby inny wektor, więc mógłbym zrobić:
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;
Z moim silnie wpisanym typemef musiałbym powiedzieć kompilatorowi, że błędne odczytanie Prędkości przez Czas skutkuje Pozycją.
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
Aby rozwiązać ten problem, myślę, że musiałbym specjalnie specjalizować każdą konwersję, co może być trochę kłopotliwe. Z drugiej strony to ograniczenie może pomóc w zapobieganiu innym rodzajom błędów (powiedzmy, pomnożenie Prędkości przez Odległość, być może nie ma sensu w tej dziedzinie). Jestem rozdarty i zastanawiam się, czy ludzie mają jakieś opinie na temat mojego pierwotnego problemu lub mojego podejścia do jego rozwiązania.
źródło
Odpowiedzi:
Są to parametry typu fantomowego , to znaczy parametry typu sparametryzowanego , które nie są używane do ich reprezentacji, ale do oddzielenia różnych „przestrzeni” typów o tej samej reprezentacji.
Mówiąc o spacjach, jest to przydatna aplikacja typów fantomowych:
Jak jednak zauważyłeś, istnieją pewne trudności z typami jednostek. Jedną rzeczą, którą możesz zrobić, to rozłożyć jednostki na wektor liczb całkowitych wykładników podstawowych składników:
W tym przypadku używamy wartości fantomowych do oznaczania wartości wykonawczych za pomocą informacji w czasie kompilacji o wykładnikach na zaangażowanych jednostkach. To skaluje się lepiej niż tworzenie oddzielnych struktur dla prędkości, odległości itp. I może wystarczyć do pokrycia twojego przypadku użycia.
źródło
Miałem podobny przypadek, w którym chciałem rozróżnić różne znaczenia niektórych wartości całkowitych i zabronić niejawnej konwersji między nimi. Napisałem taką ogólną klasę:
Oczywiście, jeśli chcesz być jeszcze bardziej bezpieczny, możesz także uczynić
T
konstruktoremexplicit
. NastępnieMeaning
jest używany w następujący sposób:źródło
Nie jestem pewien, jak działają następujące kody w kodzie produkcyjnym (jestem początkującym programistą w C ++ / programistach, np. Początkujący CS101), ale przygotowałem to za pomocą makr sys C ++.
źródło