Wie weit geht es mit typedef'ing primitiven Typen wie int

14

Ich habe C ++ - Code wie den folgenden mit vielen typedefs gesehen.

Was sind die Vorteile der Verwendung vieler typedefs im Vergleich zur Verwendung von C ++ - Grundelementen? Gibt es einen anderen Ansatz, mit dem auch diese Vorteile erzielt werden könnten?

Am Ende werden alle Daten im Speicher gespeichert oder als Bits und Bytes über die Leitung übertragen. Ist das wirklich wichtig?

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;

Es scheint logisch, zu versuchen, sicherzustellen, dass für eine bestimmte Sache immer derselbe Typ verwendet wird, um Überläufe zu vermeiden, aber ich sehe häufig Code, in dem stattdessen "int" so ziemlich überall verwendet wurde. Das typedefing führt jedoch häufig zu Code, der ungefähr so ​​aussieht:

DoSomething(EscalationLevel escalationLevel) {
    ...
}

Wodurch frage ich mich dann, welches Token den Parameter tatsächlich beschreibt: den Parametertyp oder den Parameternamen?

Kennzeichen
quelle
2
IMHO, scheint eine ziemlich sinnlose Übung, aber ich bin sicher, einige andere würden nicht zustimmen ...
Nim
1
Diese Typen sehen aus wie Variablennamen.
Captain Giraffe
11
Beachten Sie, dass dies den Eindruck erweckt, dass es typensicher ist, aber überhaupt nicht - die typedefs erstellen nur Aliase, aber nichts hindert Sie daran, zum Beispiel eine Minutean eine Funktion zu übergeben, für die ein Argument als Typ deklariert wurde Second.
Jesper
2
@Mark: schau es dir anders an. Wenn Sie einen Fehler bei der Auswahl des Integer-Typs machen oder in Zukunft neue Anforderungen auftauchen und diesen ändern möchten, möchten Sie einen einzelnen Typedef ändern oder den Code für jede Funktion durchsuchen, die ein Jahr manipuliert, und Unterschrift ändern? 640k ist genug für alle und das alles. Der entsprechende Nachteil von typedef ist, dass Menschen versehentlich oder absichtlich Code schreiben, der sich auf die Tatsache stützt, dass Year genau 16 Bit beträgt. Dann ändert sich der Code und der Code bricht ab.
Steve Jessop
1
@Steve Jessop: Ich kann mich nicht entscheiden, ob es eine gute oder eine schlechte Idee ist :-) Der erste Teil scheint dafür zu sein, der zweite dagegen. Ich denke, es hat dann Vor- und Nachteile.

Antworten:

13

Der Name eines Parameters sollte beschreiben, was er bedeutet - in Ihrem Fall die Eskalationsstufe. Der Typ ist wie der Wert dargestellt wird - das Hinzufügen von typedefs wie in Ihrem Beispiel verschleiert diesen Teil der Funktionssignatur, daher würde ich es nicht empfehlen.

Typedefs sind nützlich für Vorlagen oder wenn Sie den für bestimmte Parameter verwendeten Typ ändern möchten, beispielsweise bei der Migration von einer 32-Bit- auf eine 64-Bit-Plattform.

Björn Pollex
quelle
Dies scheint dann der allgemeine Konsens zu sein. Würden Sie sich also generell nur an "int" halten? Ich muss in dieser App sehr effizient sein, wenn es um das Übertragen von Daten geht. Wäre es also der Fall, wenn ich während der Serialisierung nur in int16_t (oder den größten für ein bestimmtes Element benötigten Darstellungstyp) konvertiere?
@Mark: Ja, so solltest du es machen. Verwenden Sie typedefs, um die Größe der verwendeten Datentypen auszudrücken, unterscheiden Sie jedoch nicht den gleichen Typ, der in verschiedenen Kontexten verwendet wird.
Björn Pollex
Danke - und nur um das zu verdeutlichen, würden Sie sich nicht die Mühe machen, int8_t anstelle von int im Code zu verwenden. Ich nehme an, meine Hauptsorge war etwas wie "Identität", die eigentlich eine von einer Datenbank generierte Identität ist. Im Moment ist es 32bit, aber ich bin mir noch nicht sicher, ob das irgendwann 64bit werden kann. Und wie wäre es mit int32_t vs int? int ist normalerweise dasselbe wie int32_t, aber es kann sein, dass es sich nicht immer um eine andere Plattform handelt. Ich denke, ich sollte einfach bei "int" allgemein bleiben und "int64_t" wo nötig .. danke :-)
@Mark: Das Wichtige an typedefs wie int32_tist, dass Sie sicherstellen müssen, dass sie korrekt sind, wenn Sie auf verschiedenen Plattformen kompilieren. Wenn Sie erwarten, dass sich der Bereich von Identityirgendwann ändert, würde ich es vorziehen, die Änderungen direkt in allen betroffenen Codes vorzunehmen. Ich bin mir jedoch nicht sicher, da ich mehr über Ihr spezielles Design wissen müsste. Vielleicht möchten Sie das zu einer separaten Frage machen.
Björn Pollex
17

Zuerst dachte ich mir "Warum nicht", aber dann kam mir der Gedanke, dass man die Sprache besser nutzen sollte, wenn man sich so sehr bemüht, um die Typen so zu trennen. Anstatt Aliase zu verwenden, definieren Sie tatsächlich Typen:

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

Es gibt keinen Leistungsunterschied zwischen:

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

und:

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

und Sie haben auch die Vorteile einer zusätzlichen Parametervalidierung und Typensicherheit. Betrachten Sie beispielsweise Code, der sich mit Geld unter Verwendung primitiver Typen befasst:

float amount = 10.00;
CallSomeFunction(amount);

Abgesehen von den Rundungsproblemen sind auch alle Typen möglich, die in ein Float konvertiert werden können:

int amount = 10;
CallSomeFunction(amount);

In diesem Fall ist es keine große Sache, aber implizite Konvertierungen können eine Quelle von Fehlern sein, die schwer zu lokalisieren sind. Die Verwendung von a typedefhilft hier nicht weiter, da es sich lediglich um einen Typ-Alias ​​handelt.

Wenn Sie einen neuen Typ vollständig verwenden, gibt es keine impliziten Konvertierungen, es sei denn, Sie codieren einen Casting-Operator. Dies ist eine schlechte Idee, da implizite Konvertierungen zulässig sind. Sie können auch zusätzliche Daten einkapseln:

class Money {
  Decimal amount;
  Currency currency;
};

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

Nichts anderes wird in diese Funktion passen, es sei denn, wir schreiben Code, um dies zu ermöglichen. Versehentliche Umbauten sind ausgeschlossen. Wir können auch komplexere Typen nach Bedarf schreiben, ohne viel Aufwand.

Skizz
quelle
1
Sie könnten sogar ein schreckliches Makro schreiben, um all diese Klassenerstellung für Sie zu erledigen. (Komm schon, Skizz. Du weißt, dass du es willst.)
1
In einigen Fällen kann eine typsichere Einheitenbibliothek hilfreich sein ( tuoml.sourceforge.net/html/scalar/scalar.html ), anstatt für jede Klasse eine eigene Klasse zu schreiben.
Steve Jessop
@ Chris - Definitiv kein Makro, aber möglicherweise eine Template-Klasse. Wie Steve betont, sind diese Klassen bereits geschrieben.
Kevin Cline
4
@ Chris: Das Makro heißt BOOST_STRONG_TYPEDEFeigentlich;)
Matthieu M.
3

Die Verwendung von typedefs für solche primitiven Typen ähnelt eher dem C-Code.

In C ++ erhalten Sie interessante Fehler, sobald Sie versuchen, Funktionen für, sagen wir, EventLevelund zu überladen Hour. Das macht die zusätzlichen Typnamen ziemlich nutzlos.

Bo Persson
quelle
2

Wir (in unserer Firma) machen das oft in C ++. Es hilft, Code zu verstehen und zu pflegen. Das ist gut, wenn Leute zwischen Teams wechseln oder umgestalten. Beispiel:

typedef float Price;
typedef int64_t JavaTimestmap;

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

Wir glauben, dass es eine gute Praxis ist, aus dem Darstellungstyp ein typedef für den Dimensionsnamen zu erstellen. Dieser neue Name repräsentiert eine allgemeine Rolle in einer Software. Der Name eines Parameters ist eine lokale Rolle . Wie in User sender, User receiver. An manchen Stellen kann es überflüssig sein, void register(User user)aber ich sehe es nicht als Problem an.

Später kommt man vielleicht auf die Idee, dass floatdie Darstellung von Preisen aufgrund der speziellen Rundungsregeln der Buchung nicht die beste ist BCDFloat. Daher lädt man einen Typ (binär codierte Dezimalzahl) herunter oder implementiert ihn und ändert den Typedef. Es gibt keine Such- und Ersetzungsarbeit float, BCDFloatdie durch die Tatsache, dass möglicherweise viel mehr Gleitkommazahlen in Ihrem Code vorhanden sind, beeinträchtigt würde.

Es ist keine Wunderwaffe und hat seine eigenen Vorbehalte, aber wir denken, dass es viel besser ist, es zu benutzen, als nicht.

Notinlist
quelle
Oder wie Mathieu M. am Posten von Skizz angedeutet hat, kann man so vorgehen BOOST_STRONG_TYPEDEF(float, Price), aber ich würde bei einem durchschnittlichen Projekt nicht so weit gehen. Oder vielleicht würde ich. Ich muss darauf schlafen. :-)
Notinlist
1

typedefIm Grunde können Sie einen Alias ​​für ein type.
Es gibt Ihnen die Flexibilität, nicht type namesimmer wieder lange zu tippen und die typeLesbarkeit zu verbessern, wobei der Aliasname die Absicht oder den Zweck von angibt type.

Es ist eher eine Frage der Wahl, ob Sie mehr lesbare Namen typedefin Ihrem Projekt haben möchten .
Normalerweise vermeide ich es, typedefprimitive Typen zu verwenden, es sei denn, sie sind ungewöhnlich lang für die Eingabe. Ich halte meine Parameternamen indikativer.

Alok Speichern
quelle
0

Ich würde so etwas niemals tun. Sicherzustellen, dass sie alle die gleiche Größe haben, ist eine Sache - aber Sie müssen sie dann nur als integrale Typen bezeichnen.

DeadMG
quelle
0

Die Verwendung solcher typedefs ist in Ordnung, solange derjenige, der sie verwendet, nichts über die zugrunde liegende Darstellung wissen muss . Wenn Sie beispielsweise ein PacketLengthObjekt an printfoder übergeben möchten scanf, müssen Sie dessen tatsächlichen Typ kennen, damit Sie den richtigen Konvertierungsspezifizierer auswählen können. In solchen Fällen fügt der Typedef lediglich einen Grad an Verschleierung hinzu, ohne etwas dafür zu kaufen. Sie hätten das Objekt genauso gut definieren können als int32_t.

Wenn Sie eine für jeden Typ spezifische Semantik erzwingen müssen (z. B. zulässige Bereiche oder Werte), sollten Sie einen abstrakten Datentyp und Funktionen für diesen Typ erstellen, anstatt nur einen Typedef zu erstellen.

John Bode
quelle