C ++ silnie typedef typedef

49

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.

Kian
źródło
Spójrz na to: zumalifeguard.wikia.com/wiki/Idtypes.idl
zumalifeguard
to samo pytanie jest tutaj: stackoverflow.com/q/23726038/476681
BЈовић

Odpowiedzi:

39

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:

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

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:

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;

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.

Jon Purdy
źródło
2
Hmm, używanie systemu szablonów do wymuszania jednostek podczas operacji jest fajne. Nie myślałem o tym, dzięki! Teraz zastanawiam się, czy możesz wymusić takie rzeczy, jak na przykład konwersja między metrem a kilometrem.
Kian
@Kian: Przypuszczalnie użyłbyś jednostek podstawowych SI wewnętrznie - m, kg, s, A i c. - i dla wygody zdefiniowałeś alias 1 km = 1000 m.
Jon Purdy,
7

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ę:

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

Oczywiście, jeśli chcesz być jeszcze bardziej bezpieczny, możesz także uczynić Tkonstruktorem explicit. Następnie Meaningjest używany w następujący sposób:

typedef Explicit<int, struct EntityIDTag> EntityID;
typedef Explicit<int, struct ModelIDTag> ModelID;
Mindriot
źródło
1
To ciekawe, ale nie jestem pewien, czy jest wystarczająco mocne. Zapewni to, że jeśli zadeklaruję funkcję z typem tekstowym, tylko odpowiednie elementy mogą być użyte jako parametry, co jest dobre. Ale dla każdego innego zastosowania dodaje składniowy narzut bez zapobiegania mieszaniu parametrów. Powiedz operacje, takie jak porównywanie. operator == (int, int) weźmie EntityID i ModelID bez reklamacji (nawet jeśli jawne wymaga, żebym go rzucił, nie powstrzymuje mnie to przed użyciem niewłaściwych zmiennych).
Kian
Tak. W moim przypadku musiałem powstrzymać się od przypisywania sobie różnego rodzaju identyfikatorów. Porównania i operacje arytmetyczne nie były moim głównym zmartwieniem. Powyższa konstrukcja zabrania przypisywania, ale nie innych operacji.
mindriot
Jeśli chcesz włożyć w to więcej energii, możesz zbudować (dość) ogólną wersję, która obsługuje również operatory, ustawiając klasę Explicit na najbardziej popularne operatory. Zobacz pastebin.com/FQDuAXdu dla przykładu - trzeba niektóre dość skomplikowane SFINAE konstruuje w celu ustalenia, czy rzeczywiście klasy otoki zapewnia zawiniętego operatorów lub nie (zobacz ten SO pytanie ). Pamiętaj, że nadal nie może obejmować wszystkich przypadków i może nie być warte kłopotów.
mindriot
Pomimo tego, że jest elegancki pod względem składni, rozwiązanie to spowoduje znaczne obniżenie wydajności dla typów całkowitych. Liczby całkowite mogą być przekazywane przez rejestry, struktury (nawet zawierające jedną liczbę całkowitą) nie mogą.
Ghostrider,
1

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 ++.

#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
źródło
Uwaga: daj mi znać o wszelkich pułapkach / ulepszeniach, o których myślisz.
Noein
1
Czy możesz dodać kod pokazujący, w jaki sposób używane jest to makro - jak w przykładach w pierwotnym pytaniu? Jeśli tak, to świetna odpowiedź.
Jay Elston,