Jak daleko posunąć się z prymitywnymi typami typowania jak int

14

Widziałem kod C ++ taki jak poniżej z wieloma typedefs.

Jakie są korzyści z używania wielu typedeftakich jak ta w porównaniu do używania prymitywów C ++? Czy istnieje inne podejście, które może również przynieść te korzyści?

Ostatecznie wszystkie dane są przechowywane w pamięci lub przesyłane przewodowo jako bity i bajty, czy to naprawdę ma znaczenie?

types.h:

typedef int16_t Version;
typedef int32_t PacketLength;
typedef int32_t Identity;
typedef int32_t CabinetNumber;
typedef int64_t Time64;
typedef int64_t RFID;
typedef int64_t NetworkAddress;
typedef int64_t PathfinderAddress;
typedef int16_t PathfinderPan;
typedef int16_t PathfinderChannel;
typedef int64_t HandsetSerialNumber;
typedef int16_t PinNumber;
typedef int16_t LoggingInterval;
typedef int16_t DelayMinutes;
typedef int16_t ReminderDelayMinutes;
typedef int16_t EscalationDelayMinutes;
typedef float CalibrationOffset;
typedef float AnalogValue;
typedef int8_t PathfinderEtrx;
typedef int8_t DampingFactor;
typedef int8_t RankNumber;
typedef int8_t SlavePort;
typedef int8_t EventLevel;
typedef int8_t Percent;
typedef int8_t SensorNumber;
typedef int8_t RoleCode;
typedef int8_t Hour;
typedef int8_t Minute;
typedef int8_t Second;
typedef int8_t Day;
typedef int8_t Month;
typedef int16_t Year;
typedef int8_t EscalationLevel;

Logiczne wydaje się staranie, aby upewnić się, że ten sam typ jest zawsze używany do konkretnej rzeczy, aby uniknąć przepełnienia, ale często widzę kod, w którym zamiast tego „int” był używany wszędzie. typedefIng często nie prowadzi do kodu, który wygląda trochę jak ten jednak:

DoSomething(EscalationLevel escalationLevel) {
    ...
}

Co sprawia, że ​​zastanawiam się, który token tak naprawdę opisuje parametr: typ parametru lub nazwa parametru?

znak
źródło
2
IMHO wydaje się dość bezcelowym ćwiczeniem, ale jestem pewien, że niektórzy inni by się nie zgodzili ...
Nim,
1
Te typy wyglądają jak nazwy zmiennych.
Kapitan Giraffe
11
Zauważ, że stwarza to wrażenie, że jest ono bezpieczne dla typu, ale wcale nie jest - typedefs po prostu tworzą aliasy, ale nic nie powstrzymuje cię przed przekazaniem na przykład funkcji Minutedo funkcji, której argument został zadeklarowany jako typ Second.
Jesper
2
@ Mark: spójrz na to z innej strony. Jeśli popełnisz błąd przy podejmowaniu decyzji o typie liczby całkowitej lub pojawią się nowe wymagania w przyszłości, a więc chcesz go zmienić, czy chciałbyś zmienić pojedynczy typedef lub chciałbyś wyszukać kod dla każdej funkcji, która manipuluje rokiem, i zmienić swój podpis? 640k wystarczy dla każdego i to wszystko. Odpowiednim minusem typedef jest to, że ludzie przypadkowo lub celowo piszą kod, który opiera się na fakcie, że Rok ma dokładnie 16 bitów, a następnie zmienia się i ich kod się psuje.
Steve Jessop,
1
@ Steve Jessop: Nie mogę się zdecydować, czy uważasz, że to dobry, czy zły pomysł :-) Pierwsza część wydaje się być za, druga przeciw. Sądzę, że ma to wady i zalety.

Odpowiedzi:

13

Nazwa parametru powinna opisywać, co to znaczy - w twoim przypadku poziom eskalacji. Typ jest reprezentowany przez wartość - dodanie typedefs, jak w twoim przykładzie, zaciemnia tę część sygnatury funkcji, więc nie polecałbym tego.

Typedefs są przydatne w przypadku szablonów lub jeśli chcesz zmienić typ używany dla niektórych parametrów, na przykład podczas migracji z platformy 32-bitowej na platformę 64-bitową.

Björn Pollex
źródło
Wydaje się to wtedy ogólnym konsensusem. Więc ogólnie trzymałbyś się „int”? Muszę być bardzo wydajny w tej aplikacji, jeśli chodzi o przesyłanie danych, więc czy byłoby to po prostu konwersja na int16_t (lub jakikolwiek największy typ reprezentacji wymagany dla konkretnego elementu) podczas serializacji?
@ Mark: Tak, właśnie tak powinieneś to zrobić. Użyj typedefs, aby wyrazić rozmiar używanych typów danych, ale nie rozróżniaj tego samego typu używanego w różnych kontekstach.
Björn Pollex,
Dzięki - i tylko dla wyjaśnienia, nie zawracałbyś sobie głowy używaniem int8_t zamiast int ogólnie w kodzie. Myślę, że moim głównym zmartwieniem było coś w rodzaju „Tożsamości”, która w rzeczywistości jest tożsamością generowaną przez bazę danych. W tej chwili jest to 32-bitowy, ale nie jestem jeszcze pewien, czy w końcu może stać się 64-bitowy. A może int32_t vs. int? int jest zwykle taki sam jak int32_t, ale może nie zawsze tak jest na innej platformie? Myślę, że powinienem po prostu trzymać się „int” ogólnie, a „int64_t” tam, gdzie jest to konieczne .. dzięki :-)
@ Mark: Ważną rzeczą w typedefs takich jak int32_tto, że musisz upewnić się, że są poprawne podczas kompilacji na różnych platformach. Jeśli spodziewasz Identitysię zmiany zakresu w pewnym momencie, myślę, że wolałbym dokonywać zmian bezpośrednio we wszystkich kodach, których dotyczy problem. Ale nie jestem pewien, ponieważ musiałbym wiedzieć więcej o twoim konkretnym projekcie. Być może zechcesz uczynić z tego osobne pytanie.
Björn Pollex,
17

Na początku myślałem „dlaczego nie”, ale potem przyszło mi do głowy, że jeśli będziesz dążył do takich długości, aby oddzielić takie typy, lepiej wykorzystaj język. Zamiast używać aliasów, faktycznie zdefiniuj typy:

class AnalogueValue
{
public:
    // constructors, setters, getters, etc..
private:
    float m_value;
};

Nie ma różnicy w wydajności między:

typedef float AnalogueValue;
AnalogValue a = 3.0f;
CallSomeFunction (a);

i:

AnalogValue a (3.0f); // class version
CallSomeFunction (a);

masz również zalety dodania sprawdzania poprawności parametrów i bezpieczeństwa typu. Na przykład rozważ kod, który dotyczy pieniędzy za pomocą prymitywnych typów:

float amount = 10.00;
CallSomeFunction(amount);

Oprócz problemów z zaokrąglaniem umożliwia także dowolny typ, który można przekształcić w liczbę zmiennoprzecinkową:

int amount = 10;
CallSomeFunction(amount);

W tym przypadku nie jest to wielka sprawa, ale niejawne konwersje mogą być źródłem błędów, które trudno ustalić. Użycie a typedefnie pomaga tutaj, ponieważ są one jedynie aliasem typu.

Użycie nowego typu całkowicie oznacza, że ​​nie ma żadnych niejawnych konwersji, chyba że kodujesz operatora rzutowania, co jest złym pomysłem, ponieważ pozwala na niejawne konwersje. Możesz także enkapsulować dodatkowe dane:

class Money {
  Decimal amount;
  Currency currency;
};

Money m(Decimal("10.00"), Currency.USD);
CallSomeFunction(m);

Nic innego nie będzie pasować do tej funkcji, chyba że napiszemy kod, aby to się stało. Przypadkowe konwersje są niemożliwe. Możemy również pisać bardziej złożone typy w razie potrzeby bez większych problemów.

Skizz
źródło
1
Możesz nawet napisać okropne makro, aby wykonać całe tworzenie klasy dla Ciebie. (No dalej, Skizz. Wiesz, że chcesz.)
1
W przypadku niektórych z nich pomocna może być biblioteka jednostek bezpiecznych dla typu ( tuoml.sourceforge.net/html/scalar/scalar.html ) zamiast pisania dla nich niestandardowej klasy.
Steve Jessop,
@Chris - Zdecydowanie nie makro, ale prawdopodobnie klasa szablonów. Jak zauważa Steve, te klasy są już napisane.
kevin cline
4
@Chris: Makro tak się nazywa BOOST_STRONG_TYPEDEF;)
Matthieu M.,
3

Użycie typedefs dla takich prymitywnych typów przypomina bardziej kod w stylu C.

W C ++ otrzymasz interesujące błędy, gdy tylko spróbujesz przeładować funkcje dla, powiedzmy, EventLeveli Hour. To sprawia, że ​​dodatkowe nazwy typów są dość bezużyteczne.

Bo Persson
źródło
2

My (w naszej firmie) często to robimy w C ++. Pomaga zrozumieć i utrzymywać kod. Co jest dobre, gdy przenosisz ludzi między zespołami lub przeprowadzasz refaktoryzację. Przykład:

typedef float Price;
typedef int64_t JavaTimestmap;

void f(JavaTimestamp begin, JavaTimestamp end, Price income);

Uważamy, że dobrą praktyką jest tworzenie typedef do nazwy wymiaru na podstawie typu reprezentacji. Ta nowa nazwa reprezentuje ogólną rolę w oprogramowaniu. Nazwa parametru jest rolą lokalną . Podobnie jak w User sender, User receiver. W niektórych miejscach może to być zbędne, void register(User user)ale nie uważam tego za problem.

Później można mieć pomysł, że floatnie jest najlepszy do reprezentowania cen ze względu na specjalne zasady zaokrąglania rezerwacji, więc pobiera się lub implementuje BCDFloattyp (dziesiętnie kodowany binarnie) i zmienia typedef. Nie ma wyszukiwania i zamiany pracy od floatdo BCDFloatktórych będzie utwardzana przez fakt, że istnieje prawdopodobnie wiele innych pływaków w kodzie.

Nie jest to srebrna kula i ma swoje własne zastrzeżenia, ale uważamy, że lepiej jest z niej korzystać niż nie.

Spoza listy
źródło
Lub, jak sugerował Mathieu M. na stanowisku Skizz, można iść BOOST_STRONG_TYPEDEF(float, Price), ale nie posunąłbym się tak daleko w przypadku przeciętnego projektu. A może chciałbym. Muszę na tym spać. :-)
Notinlist
1

typedefw zasadzie pozwala ci podać alias dla type.
Daje to elastyczność unikania type nameswielokrotnego wpisywania długich znaków i uczynienia typeich łatwiejszymi do odczytania, w których nazwa aliasu wskazuje zamiar lub cel type.

Jest bardziej kwestią wyboru, jeśli chcesz mieć bardziej czytelne nazwy typedefw swoim projekcie.
Zwykle unikam używania typedefprymitywnych typów, chyba że są one wyjątkowo długie do wpisania. Moje nazwy parametrów są bardziej orientacyjne.

Alok Save
źródło
0

Nigdy bym tego nie zrobił. Upewnienie się, że wszystkie mają ten sam rozmiar, to jedno, ale wtedy musisz tylko nazywać je integralnymi typami.

DeadMG
źródło
0

Używanie takich typedefów jest w porządku, o ile ktokolwiek z nich skorzysta , nie musi nic wiedzieć o ich podstawowej reprezentacji . Na przykład, jeśli chcesz przekazać PacketLengthobiekt do któregokolwiek printflub scanf, musisz znać jego rzeczywisty typ, aby wybrać odpowiedni specyfikator konwersji. W takich przypadkach typedef dodaje poziom zaciemnienia bez kupowania czegokolwiek w zamian; równie dobrze mógłbyś właśnie zdefiniować obiekt jako int32_t.

Jeśli chcesz wymusić semantykę specyficzną dla każdego typu (np. Dopuszczalne zakresy lub wartości), lepiej jest stworzyć abstrakcyjny typ danych i funkcje do działania na tym typie, niż tylko utworzyć typedef.

John Bode
źródło