Jetzt muss ich aber bei Verwendung der eigentlichen Nummer dahinter, Parameter eines Funktionsaufrufes etc., immer und überall einen Cast machen. Irgendwie nervt mich das langsam, weil es die Lesbarkeit beeinträchtigt. Auf der einen Seite wichtig und auf der anderen bläht das die Zeilenlänge auf.
1
(uint8_t)Gleis::GA
Ich hätte es gern ohne Cast in der Verwendung an allen Stellen im Programm.
1
Gleis::GA
Ist das ohne casten möglich in C++? Irgendwo einmalig verstecken? Ansonsten fiele mir nur ein eine Struktur anzulegen und alles von Hand durch zu nummerieren.
Ich befürchte, dass du einem alten C Hack aufgesessen bist.
Also könntest du auch die alte C Notation nutzen, welche zwar keinen Schutz gegen Böcke implementiert, aber du willst ja gerade Zweckentfremden, also genau die Böcke schießen.
Die Scoped Enums aus C++11 sind extra eingeführt worden, damit diese NICHT mehr nach int gecastet werden.
Du willst das Gegenteil, also verwende die einfach nicht.
Jetzt muss ich aber bei Verwendung der eigentlichen Nummer dahinter
Ja, warum verwendest du auch die integer/numeriusche reüräsentation das das Element des enums repräsentierst und nicht das Element selbst ?!
versuchst Du lediglich "magic numbers" einzusparen aber willst dafür keine constant verwenden?
ob ich die Verwendung in Kurzform erklären kann, ich versuche es einmal.
Selbstgebaute Platine mit H-Brücke und 8Bit I/O Portexpander usw.
Damit werden Gleisabschnitte auf der Modelleisenbahn ein- und ausgeschalten.
Sieht dann bspw. so aus.
Also aktuell entweder
den Cast in die Klasse verschieben. Nur dann muss ich bei Datentypänderung die Klasse anpassen. Ob das passieren kann weiß ich aktuell nicht. Außer vielleicht "auto" funktioniert.
Oder ich erstelle eine Struktur für die "Magic Numbers". Jedenfalls benötige ich letztlich Integer für die Funktionsparameter hBridge.gleisOn(n) bzw. hBridge.gleisOff(n).
In allen Fällen habe ich noch den Cast denn man aktuell bei Cpp nicht mehr machen soll.
Ich möchte das jetzt möglichst sauber haben wenn ich jetzt nochmal daran rumfummel.
Wenn ich einzelne Funktionen schreibe mit sprechenden Namen, dann wären darin als Parameter Magic Numbers drin. Gut, die könnte man kommentieren.
Bspw. spontan
1
gleisA_einschalten(){hBridge.gleisOn(0);}
2
gleisA_auschalten(){hBridge.gleisOff(0);}
Weiß jetzt nicht ob das eine gute Idee ist. Was tun?
Das mit dem namespace Gleis von Ernst ginge eigentlich auch.
Sieht dann bspw. so aus.hBridge.gleisOn((uint8_t)Gleis::GA);
hBridge.gleisOn((uint8_t)Gleis::GC);
Dann hat deine Funktion gleisOn() aber als Parametertyp nicht Gleis, sondern wohl uint8_t. Warum, wenn sie doch ein Gleis haben will?
Wenn es unbedingt uint8_t sein soll, warum verwendest du dann enum class? Das ist ja gerade dazu da, dass es sich nicht ohne Cast in einen Integer konvertieren lässt.
Sieht dann bspw. so aus.hBridge.gleisOn((uint8_t)Gleis::GA);
hBridge.gleisOn((uint8_t)Gleis::GC);
Dann hat deine Funktion gleisOn() aber als Parametertyp nicht Gleis,
sondern wohl uint8_t.
Genau, sie hat uint8_t als Parametertyp.
Warum, wenn sie doch ein Gleis haben will?
Dann wandert der Cast in die Klasse. Dann ist es aus den Augen aus dem Sinn. Ist das eine gute Idee? Irgendwo und irgendwann muss ich aktuell casten wenn ich dafür enum verwenden.
Edit:
Klasse angehangen, hoffe das hilft, wobei da noch mehr dran hängt
Dann wandert der Cast in die Klasse. Dann ist es aus den Augen aus dem
Sinn. Ist das eine gute Idee?
Ja. Wenn die Funktion ein Gleis haben will und dies durch den gleichnamigen enum repräsentiert wird, dann sollte sie diesen auch als Parametertyp haben.
Irgendwo und irgendwann muss ich aktuell casten wenn ich dafür enum
verwenden.
… wenn du enum class verwendest. Wenn du nur enum verwendest, geht es ohne Cast.
Dann wandert der Cast in die Klasse. Dann ist es aus den Augen aus dem
Sinn. Ist das eine gute Idee? Irgendwo und irgendwann muss ich aktuell
casten wenn ich dafür enum verwenden.
Ja, die Transformation Gleis->int gehört in gleisOn.
Gleis ist ein abstraktes Objekt (enum), setPin ein konkreter HW-Pin.
Es gibt 3 Möglichkeiten:
casten
enum (ohne class)
Umwandlungsfunktion, z.B. uint8_t GleisToPin(Gleis g)
Sobald Du mehrere H-Brücken hast oder Lücken, beginnen die Trixereien. Eigentlich ist eine Umwandlungsfunktion sauberer, da die Reihenfolge der Enums dann nicht der HW entsprechen müssen und der Code auf andere Setups passt. Aber natürlich macht das in der Praxis jeder so.
Das Argument von gleisOn und gleisOff ist ein Gleis und sollte damit
logischerweise vom Type Gleis (enum) sein. Das Argument von setPin und
clearPin ist eine Pinnummer, also eine Integer-Zahl. Deswegen sollte der
Cast meiner Meinung nach genau dazwischen stattfinden, also so:
Ist das ohne casten möglich in C++? Irgendwo einmalig verstecken?
Ansonsten fiele mir nur ein eine Struktur anzulegen und alles von Hand
durch zu nummerieren.
Nur weil es plötzlich in C++ sehr viele neue Dinge gibt, muß man die nicht krampfhaft nutzen, wenn die nicht passen.
Bleib beim enum ohne class, oder ändere die Logik so, daß die ohne deren Integerwerte funktioniert. Beides zusammen ist Krampf.
Das hier finde ich auch gut, vor allem dann, wenn die Konvertierung an
mehr als zwei Stellen im Code erfolgen soll:
Insbesondere kann man die Funktion ja inline realisieren, dann hat man erstmal keinerlei Overhead. Falls, wie oben angedeutet, später doch mal Gleisnummer != Pinnummer wird aus irgendeinem Grund, muss man nur noch genau diese Funktion anpassen. Ist eine saubere logische Trennung der einzelnen Funktionen.
Nein. Das steht so nirgends, auch wenn beide häufig äquivalent sein dürften.
Davon abgesehen, auch ein "unsigned char" ist ein "integer type":
"There are five standard signed integer types, designated as signed char, short int, int, long int, and long long int." (Die "unsigned" pendants gibt es natürlich auch alle.)
Der Datentyp eines uint8_t ist kein integer im C-Standard sondern ein
unsigned char, mehr wollte ich nicht sagen.
Ein unsigned char ist ein integer.
Ein integer Datentyp (int type) ist im C-Standard mindestens 16 Bit
lang!
Nochmal, du verwechselt da den Typ int mit einem integer allgemein. Nicht jeder integer ist vom Typ int, denn es gibt mehrere integer-Typen. int ist einer davon, unsigned char ein anderer. Für int gilt deine Aussage, aber sie gilt nicht für jeden integer-Typ.
Hier paßts aber ziemlich gut, Herr Fortschrittsverweigerer.
Nein, es passt nicht, wenn die enums nur etwas aufgemotzte Integer-Werte sind, und die ganze Anwendung eigentlich auf Integer angewiesen ist. Dafür ist enum class halt nicht so richtig passend. Die Alternative mit einem enum in einem namespace wurde ja oben auch schon genannt.
Die eigentliche Frage ist aber doch, warum überhaupt die Integerwerte benötigt werden.
Sauber wäre eine Implementierung, in der die Anwendung nur und ausschließlich mit den enum-Werten arbeitet. Solange die Gleise keine Reihenfolge haben müssen, oder eine Sortierung notwendig ist, braucht es da überhaupt keine Integer (und selbst wenn, geht das). Da klappt das dann auch problemlos mit class enum.
Wenn doch irgendwo nahe an der Hardware Integer benötigt werden, dann gehört die Umwandlung an eine einzige Stelle in den "Treiber", der dann direkt an den I/Os rumfummelt.
dann gehört die Umwandlung an eine einzige Stelle in den "Treiber",
der dann direkt an den I/Os rumfummelt.
... was bereits mehrfach vorgeschlagen wurde.
Wobei es natürlich nichtsdestotrotz hilfreich ist, wenn man sie gleich so definiert, dass ihre Zahlenwerte „zufällig“ der Hardware entsprechen, solange das möglich ist – das reduziert die Abstraktionsschicht rein physisch dann auf Null.
Reine c enums haben den Nachteil das sie global sichtbar sind.
Weil enums zur Kompilezeit konstant sind kannst du sie in C++ anonym in eine struct verpacken.
wie Einige gelesen haben, ist die Zuordnung der Gleisabschnitte zur Hardware (Portexpander) passend gemacht. Sprich die Verdrahtung entspricht der enum Reihenfolge bzw. umgekehrt. Auf dem Bild unten links ist die besagte Platine. Schaltplan wegen der H-Brückenfragen vs. Gleise hängt auch dran.
Ich tendiere aktuell zur Lösung mit namespace enum oder struct enum. Sodass ich auf den Cast, egal wo er stehen würde ganz verzichten kann. Dennoch habe ich damit eine Kapselung, sodass man die enum Namen nicht völlig wild verwenden kann. Betrifft sowieso nur mich der das schreibt und lesen muss und der sich selbst vor groben Dummheiten schützen möchte. ;-)
So eine Modelleisenbahn zu programmieren ist schon eine andere Nummer. Es gibt immer wieder neue Ideen und Änderungen.
Dazu gleich noch eine Frage zum inkludieren. Auf die Gefahr hin das es wohl keine eindeutige Antwort geben könnte.
Wie macht ihr das gewöhnlich, wenn eine Klasse eine andere Klasse benötigt und inkludiert werden muss.
Inkludiert ihr direkt in der Klasse die diese benötigt oder schreibt ihr alle #includes ins Hauptprogramm?
Wobei man hier wiederum das Problem haben kann das man tunlichts auf die Reihenfolge achten muss.
Die Frage dreht sich eher darum ob man allen Lesern im Hauptprogramm zeigt, welche Klassen benötigt werden oder ob der Leser sich diese Information bei Bedarf in den Header etc. selbst zusammensuchen soll?
Hier paßts aber ziemlich gut, Herr Fortschrittsverweigerer.
Nein, es passt nicht, wenn die enums nur etwas aufgemotzte Integer-Werte
sind, und die ganze Anwendung eigentlich auf Integer angewiesen ist.
Du kannst das gerne andes sehen, aber ich halte Typsicherheit und Lesbarkeit für wichtig. Und eine Enum an dieser Stelle zu nutzen, hilft beiden Aspekten. Sonst kann man auch Magic Numbers nehmen und mit ein bissel Unkonzentriertheit bekommt man Magic Smoke.
Inkludiert ihr direkt in der Klasse die diese benötigt oder schreibt ihr
alle #includes ins Hauptprogramm?
Natürlich in der Header-Datei der Klasse bzw. der Implementierungsdatei der Klasse, je nachdem wo der vollständige Typ gebraucht wird. Um Zyklen aufzulösen, kann man Header mit nur unvollständigen Deklarationen machen (s.a. <iosfwd> statt <iostream>).
An anderer Stelle (im "Hauptprogramm") darf der Anwender der Klasse nicht über eine Reihenfolge nachdenken müssen!
okay, Danke euch. Habe nochmal inne gehalten und mich für struct enum entschieden. Ich denke das ist der passende Weg für mich. Damit kann ich solche Konstrukte wie bspw.
Das hilft mir aufzuräumen, Typsicherheiten beizubehalten und es lesbarer zu bekommen. Zog sich ja noch weiter durch das Programm. include Frage ist auch beantwortet. Danke an alle.
Bevor der Nächste meckert, die Magic Number 1 bzw. -1 kommt auch noch weg für CW bzw. CCW. Ist ja alles noch "under construction" inkl. Spieltrieb zwischendurch. :-)
Wenn das struct "Gleis" heisst, ist es unlogisch für Gleis A dann das enum GA zu nennen. Besser wäre Gleis.A. Das "G" in "GA" ist redundant. Gleiches bei den Weichen.
du hast völlig recht Wilhelm. Nun kommt jedoch das aber. Kannst du dich erinnern an meinen Thread zu den #defines? Das Problem ist das im DxCore Namen wie A1 für Pins schon mittels #define existieren und dann kollidiert das alles mit meinen Namen, wegen Präprozessor. Deswegen bin ich den verschmerzbaren Umweg gegangen einen Anfangsbuchstaben zu wählen. Ich habe mich daran schnell gewöhnt.
du hast völlig recht Wilhelm. Nun kommt jedoch das aber. Kannst du dich
erinnern an meinen Thread zu den #defines? Das Problem ist das im DxCore
Namen wie A1 für Pins schon mittels #define existieren und dann
kollidiert das alles mit meinen Namen, wegen Präprozessor. Deswegen bin
ich den verschmerzbaren Umweg gegangen einen Anfangsbuchstaben zu
wählen. Ich habe mich daran schnell gewöhnt.
Naja, das gehört ja ohnehin zu Deiner enum und ist durch diese Zugehörigkeit doch schon absolut eindeutig bezeichnet?! Das ist schon was anderes als die eher stumpfen Textersetzungen eines Präprozessors, methinks.
Naja, das gehört ja ohnehin zu Deiner enum und ist durch diese
Zugehörigkeit doch schon absolut eindeutig bezeichnet?! Das ist schon
was anderes als die eher stumpfen Textersetzungen eines Präprozessors,
methinks.
Nun, wenn irgendwo ein #define mit dem Namen existiert, dann kann dieser Name nirgendwo mehr für was anderes verwendet werden, auch nicht als Enum-Wert.
Damit kann ich
solche Konstrukte wie bspw.
...
ControlWeiche weiche [] {
{ 1, (uint8_t)Servoname::WBCL, (uint8_t)Sensorboard::SD1,
(uint8_t)Sensorboard::SC1, (uint8_t)Sensorboard::SB1},
...
ändern in
...
ControlWeiche controlWeiche [4] {
{ 1, servo.WBCL, sensor.SD1, sensor.SC1, sensor.SB1},
Das kannst du auch, wenn du enum class Typen verwendest. ControlWeiche darf dann keine Integer, sondern deine enum class Typen enthalten. Nur der Treiber, der dann mit den Daten aus CotrolWeiche die I/Os bedient, muß wissen, welche Pins angesteuert werden müssen. Der macht dann die Umsetzung von der enum class Typen in Integer. Alle Ebenen da drüber brauchen das nicht.
Habe nochmal inne gehalten und mich für struct enum
entschieden.
Finde ich gut. In C erzwinge ich die strenge Typprüfung oft so (auch wenn enums selber global sind)
1
enumeGleis{GA,GB,GC,GD,GE,GF,GZ,NumberOfGleise};
2
3
typedefstruct
4
{
5
enumeGleisnr;
6
}GleisType;
7
8
voidgleisOn(GleisTypeg){icGleis.setPin(g.nr);}
9
voidgleisOff(GleisTypeg){icGleis.clearPin(g.nr);}
10
11
intmain(void)
12
{
13
GleisTypeGleisWest={GC};
14
SensorTypeSensorOben={GC};/* <-- nur Warnung */
15
16
gleisOn(GleisWet);
17
gleisOn(SensorOben);/* <-- Compilerfehler, selbst mit Warnungen aus */
18
}
Bei Gleisen mag das wie Overkill aussehen, verhindert aber das ein Sensor übergeben wird. Vielleicht wird es anschaulicher, wenn man im Programm Auftrags- und Kundennummer hat und beide u32 sind. Irgendwann hat jemand in einem Sub-Block nur noch "Nr" und der nächste Maintainer ruft damit "CheckOrder(Nr)" statt "CheckCustom(Nr)" auf.
wie Einige gelesen haben, ist die Zuordnung der Gleisabschnitte zur
Hardware (Portexpander) passend gemacht. Sprich die Verdrahtung
entspricht der enum Reihenfolge bzw. umgekehrt.
Hardware hat die Eigenschaft, sich während der Entwicklung mehrfach zu ändern oder später erweitert zu werden, da kann einem eine feste Zuordnung schnell auf die Füße fallen.
Ich versuche daher, die Pinzuweisung variabel zu halten und nur in einem Header zuzuweisen. Dann kann man auch beim Routen der Platine noch Pins vertauschen, um Vias zu sparen. Weniger Vias machen das Layout übersichtlicher bei der Fehlersuche und beim Patchen.
Für Pins, die über Expander bedient werden, lege ich daher Schattenvariablen im RAM als Bit-Struct an, die ausschließlich verwendet werden.
Hier mal ein Beispiel:
Definition der Bitstruct:
Der AVR-GCC kann die Bitzugriffe sehr gut optimieren, d.h. hält die Bytes in Registern, wenn sie mehrfach benötigt werden.
Mit Enums dürfte das aber ein riesen Codemonster werden (bezüglich Schreibarbeit als auch Binärcode).
update_all(); schreibt und liest die Expander, d.h. man muß nicht für jedes einzelne IO-Bit Zeit verschwenden. Eingänge bleiben dadurch während des gesamten Zyklus konsistent. Obendrein dürfen Outputs mehrfach geändert werden, ohne daß es zu Glitches kommt.
Nun, wenn irgendwo ein #define mit dem Namen existiert, dann kann dieser
Name nirgendwo mehr für was anderes verwendet werden, auch nicht als
Enum-Wert.
Inkludiert ihr direkt in der Klasse die diese benötigt oder schreibt ihr
alle #includes ins Hauptprogramm?
Jede Header (.h-Datei) enthält alle #includes, die es braucht. Auch wenn das nicht zwingend ist, sollte es so sein. Natürlich mit #Include-Guard in welcher Form auch immer.
Jede Klasse (.c-Datei) enthält alle #includes, die es braucht. Direkt oder Indirekt. (Zwingend, logisch)
Wenn ich irgendwo die Klasse "Gleis" verwenden will, dann muss "Gleis.h" reichen. Wenn auch "Schiene.h" oder "Gleisbett.h" benötigt werden, muss Gleis.h die mitbringen.
Ausnahme sind manchmal projektspezifische Konfigurationsdateien ("config.h") am Anfang jeder Applikations-.c.
Bei Instanzen (GleisA in "GleisA.h", GleisB in "GleisB.h") kann es sein, dass die nur in einer Datei (main.c) benötigt werden und alle anderen mit der Oberklasse "Gleis.h" zufrieden sind. Dann ist Gleis.h in GleisA.h und nicht umgekehrt. Und beide parallel in main.c.
Nun, wenn irgendwo ein #define mit dem Namen existiert, dann kann dieser
Name nirgendwo mehr für was anderes verwendet werden, auch nicht als
Enum-Wert.
True, aber wer würde denn sowas machen? :-)
Es ist ist wie es ist. An dem DxCore fummel ich nicht dran rum. Der wird nur angewendet. Mehr möchte ich dazu gar nicht schreiben. Das Thema ist durch.
Damit kann ich
solche Konstrukte wie bspw.
...
ControlWeiche weiche [] {
{ 1, (uint8_t)Servoname::WBCL, (uint8_t)Sensorboard::SD1,
(uint8_t)Sensorboard::SC1, (uint8_t)Sensorboard::SB1},
...
ändern in
...
ControlWeiche controlWeiche [4] {
{ 1, servo.WBCL, sensor.SD1, sensor.SC1, sensor.SB1},
Das kannst du auch, wenn du enum class Typen verwendest. ControlWeiche
darf dann keine Integer, sondern deine enum class Typen enthalten. Nur
der Treiber, der dann mit den Daten aus CotrolWeiche die I/Os bedient,
muß wissen, welche Pins angesteuert werden müssen. Der macht dann die
Umsetzung von der enum class Typen in Integer. Alle Ebenen da drüber
brauchen das nicht.
wie Einige gelesen haben, ist die Zuordnung der Gleisabschnitte zur
Hardware (Portexpander) passend gemacht. Sprich die Verdrahtung
entspricht der enum Reihenfolge bzw. umgekehrt.
Hardware hat die Eigenschaft, sich während der Entwicklung mehrfach zu
ändern oder später erweitert zu werden, da kann einem eine feste
Zuordnung schnell auf die Füße fallen.
Ich versuche daher, die Pinzuweisung variabel zu halten und nur in einem
Header zuzuweisen.
Eigentlich bin ich absolut flexibel. Bin jetzt ganz verunsichert. Falls ich die Verdrahtung ändere, was wohl nicht passieren wird, muss ich nur die Namen im enum umsortieren. Denn die Reihenfolge entspricht der Bitnummer 0...7 vom I/O des PortExpanders. Das hier meine ich, kann ich jederzeit umsortieren wenn es denn sein müßte. Bei den Sensoren 0...15, sind noch paar frei.
Eigentlich brauchst du doch eine Abbildung, die Gleise auf H-Brücken
abbildet?
Da stellt sich mir die Frage, warum überhaupt erstmal Enums anlegen, wenn dann doch wieder alles umständlich umgemappt werden muß. Das kostet nur unnütz Schreibarbeit und Code und verschlechtert die Lesbarkeit rapide.
Man kann doch gleich sprechende Symbole anlegen, denen sofort die konkrete Byteadresse und Bitnummer mitgeteilt wird.
Sowas mag ich nun überhaupt nicht, magische Nummern, da geht doch der Überblick komplett verloren. Es sollte schon leicht zu erkennen sein, welcher IO-Port und welches Bit da angesprochen wird. Ansonsten sucht man sich dumm und dämlich nach Zuordnungsfehlern.
ich habe das Gefühl aktuell dreht man sich im Kreis. Nicht böse gemeint.
Enums, weil das für mich die kürzeste Form ist sprechende Symbole abzubilden. Die werden automatisch durchnummeriert in der Reihenfolge wie ich diese benötige.
ist mehr Tipparbeit und Fehlerträchtig, empfinde ich.
Auch die Bitstruct Idee habe ich mir überlegt. Jedoch ist es auch mehr Schreibaufwand ähnlich dem obigen struct Gleis. Ich sehe da leider keinen Vorteil drin. Wenn man sich hierbei vertippt sucht man sicherlich auch lange Zeit den Fehler. Ich wüßte jetzt ehrlich gesagt nicht was diese eine Zeile
1
structGleise{enum{GA,GB,GC,GD,GE,GF,GZ,Anzahl};};
toppen könnte. Hier steckt alles drin was benötigt wird. Der "Wert" wird dem Objekt übergeben was den Portexpander verwaltet und der weiß was zu tun ist. Hier muss nichts umgemappt werden. Ist genauso direkt verwendbar wie Bitstrukturen. Vielleicht habe ich einen Tunnelblick, vielleicht haben andere einen Tunnelblick. Kann ich nicht beurteilen. Nur eins weiß ich. Mir gefallen die geänderten struct-Enums ohne Cast und dabei werde ich bleiben. Einfach zu schreiben und sicher anzuwenden.
Nach aller vorheriger Konfigurationsorgie ist die Hauptschleife dafür allein gesehen praktisch wie leer.