Ein Merkmal von guten Quelltexten ist deren Lesbarkeit bzw.
Nachvollziehbarkeit. Bei meinen ersten Gehversuchen mit C (AVR
Prozessoren) sah dann beispielsweise das Ein- uns Ausschalten einer LED>
etwa so aus:
1
PORTB|=0b00000010;// LED an
2
...
3
PORTB&=~0b00000010;// LED aus
Zugegebenermaßen gruselig, hat mir auch einiges Nachdenken gekostet, als
ich aus Versehen statt 0b mal 0x vorne dran stehen hatte - gab "nur"
eine Warnung, ging aber trotzdem nicht. Das sind so Fehler die man nur
einmal macht ;-)
Schaut man sich Codebeispiele von anderen an, sieht es oft so in der Art
aus:
1
#define LED PB1
2
...
3
PORTB|=1<<LED;// LED ein
4
...
5
PORTB&=~(1<<LED);// LED aus
Besser? Zumindest etwas besser wartbar. Für manche immer noch kryptisch,
wenn man die Grundregeln der Bit-Manipuliererei einmal verstanden hat
aber ganz ok.
Manche gehen einen Schritt weiter und schreiben sowas in der Art:
1
#define LED_ON PORTB |= 1<<PB1;
2
#define LED_OFF PORTB &= ~(1<<PB1);
3
...
4
LED_ON;
5
...
6
LED_OFF;
Vielleicht am besten lesbar, aber eigentlich kein "richtiges" C mehr(?)
Hab jedenfalls den Eindruck bei sowas scheiden sich die Geister.
Welche Art, solche Sachen zu coden hatet ihr für optimal bzgl.
Lesbarkeit und Wartbarkeit - eher sparsamen Einsatz des Präprozessors
oder extensiven Gebrauch dieser Möglichkeit?
Micha schrieb:> Das sind so Fehler die man nur> einmal macht ;-)
Sicher?
Man kann sich da gerne mal um eine Bitstelle verzählen.
Man kann Ports aber auch als Struct von Bitvariablen definieren.
Das finde ich am besten lesbar.
Beitrag "Re: Problem mit Befehl SET"
@ Micha (Gast)
> LED_ON;> ...> LED_OFF;>Vielleicht am besten lesbar,
Eben!
>aber eigentlich kein "richtiges" C mehr(?)
Doch!
>Hab jedenfalls den Eindruck bei sowas scheiden sich die Geister.
Das tun sie sich immer.
>Welche Art, solche Sachen zu coden hatet ihr für optimal bzgl.>Lesbarkeit und Wartbarkeit
Siehe oben! Bei der Programierung will ich bestimmte Aktionen auslösen,
wie z.B. die LED schalten. Mit welchem Pin das passiert, ist mir dann
egal! UNd ich ich will es auch nicht dauernd hinschreiben müssen!
Vor allem, wenn man die Pinbelegung mal ändern will (Projektänderung,
neues Projekt basierend auf bestehendem Code)
> - eher sparsamen Einsatz des Präprozessors
Nein.
>oder extensiven Gebrauch dieser Möglichkeit?
Man sollte eine gesunde Mischung anstreben. Extreme sind selten gut.
Micha schrieb:
[schnipp]
> Manche gehen einen Schritt weiter und schreiben sowas in der Art:>
1
>#defineLED_ONPORTB|=1<<PB1;
2
>#defineLED_OFFPORTB&=~(1<<PB1);
3
>...
4
>LED_ON;
5
>...
6
>LED_OFF;
7
>
> Vielleicht am besten lesbar, aber eigentlich kein "richtiges" C mehr(?)> Hab jedenfalls den Eindruck bei sowas scheiden sich die Geister.> Welche Art, solche Sachen zu coden hatet ihr für optimal bzgl.> Lesbarkeit und Wartbarkeit - eher sparsamen Einsatz des Präprozessors> oder extensiven Gebrauch dieser Möglichkeit?
Die Frage ist falsch gestellt.
Das Ziel sollte immer beste Lesbarkeit des Codes sein. Weil das i.d.R.
gleichzeitig auch die beste Wartbarkeit, die beste Nachnutzbarkeit und
die geringste Fehleranfälligkeit bedeutet.
Ob dieses Ziel durch den Einsatz des Präprozessors erreicht wird, durch
die Definition entsprechender Funktionen in der verwendeten Hochsprache
1
inlinevoidLED_on(void){
2
PORTB|=1<<PB1;
3
}
4
...
5
LED_on();
oder noch ganz anders, ist dabei Nebensache. Außer wenn die verwendeten
Sprachmittel selber wiederum die Lesbarkeit beeinträchtigen.
In C verwendet mal halt traditionell eher den Präprozessor, weil da auch
mit Uralt-Compilern schneller Code bei rauskommt. Aktuelle Compiler
können Inlining von Wrapper-Funktionen wie oben automatisch. An der
Stelle will man dann vielleicht lieber die Funktion nutzen.
Persönlich glaube ich, daß Aussagen der Art "der Präprozessor ist pöhse"
hauptsächlich von Leuten kommen, die den Präprozessor nicht verstehen
und (deshalb) Angst davor haben.
XL
Schaulus Tiger schrieb:> Unverständlich ist, warum jemand freiwillig so viele "<<" und "()"> eintippt.
Spätestens wenn du Bits nicht als Maske, sondern als Bitnummer brauchst,
bist du froh drüber, wenn die I/O-Pins als Bits und nicht als Maske
definiert wurden. Denn vom Bit zur Maske ist erheblich einfacher als
umgekehrt (Hausaufgabe: wie geht das ohne Laufzeitrechnung ;-).
Eine Stelle, wo man Bits benötigt, ist beispielsweise die
Bitadressierung bei einigen Cortex-M Cores.
A. K. schrieb:> Schaulus Tiger schrieb:>> Unverständlich ist, warum jemand freiwillig so viele "<<" und "()">> eintippt.>> Spätestens wenn du Bits nicht als Maske, sondern als Bitnummer brauchst,> bist du froh drüber, wenn die I/O-Pins als Bits und nicht als Maske> definiert wurden.
Ich denke, die Anmerkung war ein Plädoyer dafür, die angeborene Faulheit
aller Programmierer auch in dem Sinne auszuleben, dass man die << und ()
genauso in Makros versteckt. War A sagt, sollte nicht bei B stehen
bleiben und auch C sagen.
Wogegen prinzipiell ja nichts zu sagen ist, solange der Punkt nicht
überschritten wird, an dem man dann schon wieder zu viel in Makros aus
dem eigentlichen Code verbannt und den gegenteiligen Effekt erzielt.
Dann wird der Makro-verwendende Code nicht mehr einfacher (im Sinne von
einfacher zu lesen), sondern nur noch kryptischer.
Micha schrieb:> oder extensiven Gebrauch dieser Möglichkeit?
Wenn ich F_CPU ändere, werden sämtlich Einstellungen, die davon abhängig
sind, automatisch übernommen.
Z.B. so:
A. K. schrieb:> Der Compiler sortiert konstante Bedingungen auch> selber aus.
Hab ich mir jetzt schon dreimal durchgelesen und immer noch nicht
verstanden.
mfg.
Thomas Eckmann schrieb:> Hab ich mir jetzt schon dreimal durchgelesen und immer noch nicht> verstanden.
Ein guter Optimizer sorgt dafür, dass bei
1
if((F_CPU/2)<200000)
2
...(1<<ADPS0)...
3
elseif((F_CPU/4)<200000)
4
...(1<<ADPS1)...
vom Aufwand her circa das Gleiche rauskommt wie bei dir, selbst wenn du
das in einer Funktion vergräbst, die er dann inlinen darf. Die
Bedingungen sind ja konstant, weshalb nur einer der Zweige überhaupt zu
Code gerinnt.
A. K. schrieb:> vom Aufwand her circa das Gleiche rauskommt wie bei dir, selbst wenn du> das in einer Funktion vergräbst, die er dann inlinen darf. Die> Bedingungen sind ja konstant, weshalb nur einer der Zweige überhaupt zu> Code gerinnt.
Das hellt die Sache ein wenig auf. Kann man auch so machen und der
Compiler wird da auch mitspielen.
Aber unter dem Satz zuvor konnte ich mir absolut nichts vorstellen.
mfg.
Thomas Eckmann schrieb:> Aber unter dem Satz zuvor konnte ich mir absolut nichts vorstellen.
Es gibt Aufgaben, die man gleichermassen mit Präprozessor wie mit dem
Compiler selbst erledigen kann, oder es eigentlich können sollte. Dein
Beispiel gehört dazu.
Und es gibt welche, wo das nicht geht, weil der Compiler dabei auf die
Nase fällt. Wenn man mit dem Präprozessor beispielsweise Spezifika der
verwendeten Umgebung unterscheidet, also Compiler, Zielmaschine etc.
Denn da ist gewöhnlich nur einer der Zweige überhaupt compilierbar, die
anderen laufen auf Syntaxfehler, fehlende Definitionen aus fremden
Includes etc.
Stroustrup, kein grosser Freund des Präprozessors, hatte sich in C++
etwas Gedanken darum gemacht, wie man den Präprozessor aus der ersten
Kategorie verbannen kann. Was zu echten Konstanten führte, weshalb also
N von
const int N = 10;
in C++ oft an Stellen verwendbar ist, die lexikalische Konstanten
erwarten, während das in C nicht geht.
Ich verzichte fast komplett auf den Präprozessor. Nur für bedingte
Kompilierungen setzte ich ein
#ifdef XXXX
ein.
Die Makros führten nur dazu, dass der Code schwerer zu verstehen und zu
warten ist. Es gibt da nämlich eine ganze Reihe von bizarren
Fehlermöglichkeiten, falls man Makros benutzt.
Ich nehme lieber richtige Funktionen und Klassen, in der Hoffnung, dass
der Compiler das schon optimieren wird. Und Rechenlesitung ist doch oft
genug vorhanden.
Und bislang bin ich mit dieser Vorgehensweise ganz gut gefahren.
Micha schrieb:> Ein Merkmal von guten Quelltexten ist deren Lesbarkeit bzw.> Nachvollziehbarkeit.> Welche Art, solche Sachen zu coden hatet ihr für optimal bzgl.> Lesbarkeit und Wartbarkeit - eher sparsamen Einsatz des Präprozessors> oder extensiven Gebrauch dieser Möglichkeit?
Für eine gute Lesbarkeit des Codes ist eine sinnvolle Struktur
unabdingbar. Wichtig sind Interfacedefinitionen, die die Aufgaben klar
verteilen und die verwendeten Parameter definieren. Der Umzug auf eine
neue Hardware kommt erfahrungsgemäß genau dann, wenn man sie nicht
erwart. Daher sollten zum Port und Pin-Definitionen auch als solche
erscheinen.
Ob das dann mit einer Maske wie 0x0040 oder (1<<6) erfolgt ist egal
solange es konsistent ist.
Probleme der Wartbarkeit gibt es, wenn die Aufgaben nicht klar verteilt
werden und State Machines nicht klar implementiert werden.
Deutlich weniger wichtig ist die Verwendung der Mittel Funktion und
Präprozessor.
Präprozesor-Zauberei schreibt sich manchmal gut hin. Es gibt aber viele
Fälle, bei denen diese schwerer zu lesen ist, als eine (inline-)
Funktion, die der GCC (siehe Forumname) bequem zu optimierten Code
umwandelt.
Roland Praml schrieb:> hier ein Beispiel wie man es wirklich übertreiben kann
Naja, den IOCCC zu zitieren, passt nun wirklich nicht zur ernst
gemeinten Frage (auch wenn das Pseudo-Java natürlich wirklich hübsch
ist, aber sonst wär's auch kein IOCCC-Preisträger geworden).
Ich nehme an, daß es bei der Frage um größere Projekte geht, wo es
wirklich auf die Lesbarkeit (für jedermann?) ankommt.
Dazu wurde schon das Detail von den Profis gesagt ;)
Vor ca. 2 Jahren habe ich angefangen, dem AVR im makefile mitzuteilen,
bitte nur bedingt zu kompilieren. Das hat zwar funktioniert, war aber
eine elende Schinderei. Es ging dabei um eine Kleinserie von fast
identischen Funktransceivern, und das "fast" hat einen sogar als
Privatanwender fast den letzten Nerv gekostet, weil man immer irgendwann
vergessen hat, daß man das hinzugekommene Stück Assembler oder C nun
doch auch noch in den anderen Modulen unterbringen kann.
Inzwischen habe ich mich vom makefile über die "global.h" zur "main.h"
vorgearbeitet.
Das bedeutet, daß in meinen STM32-Projekten momentan tatsächlich alles
im Ordner "common" landet, bis auf "main.c" und "main.h".
Das bedeutet, daß man man bei einer Veränderung der "Basisbibliotheken"
in "common" immer auch die darauf zugreifenden "main.h" und "main.c"
ändern muß. Aber mehr auch eben nicht. Das macht es einigermaßen
überschaubar.
Weil ich verschiedene Funktransceiver und auch mehrere Displays
verwalten will, fühle ich mich mit der derzeitigen Lösung wohl. Alle
wichtigen Schalter liegen in "main.h". Unetrschiedliche Anforderungen in
"main.c".
Allerdings habe ich bisher darauf verzichtet, die beiden
Prozessorfamilien AVR und STM32 in der "main.h" auch noch zu
berücksichtigen.
Nun pflege ich die STM32-103er auf der einen Seite, und die AVR- und
STM32F4- auf der anderen Seite. Bei den beiden letztgenannten will ich
zumindest deren header-Files kompatibel halten. Es ist ein Kreuz.
Und ich fürchte, daß das nur ein weiterer Anfängerfehler ist.
Die Abstraktionsebene, wo WiFi-Komponenten zusammen mit Bluetooth und
dem (von mir heißgeliebten) RFM12 sowiee verschiedenen Prozessortypen
allein in einer "main.h" geschaltet werden sollen, wird wahrscheinlich
nicht funktionieren.
Aber mir gefällt die Frage, weil ich sie mir vor kurzem selbst gestellt
habe, als ich die "main.h" mal auf Überlänge bzw. Übersichtlichkeit maß
g
Es kommt vermutlich auch darauf an, was von Outgesourcten an Beiträgen
kommt. Wenn da jeder seinen eigenen Stiefel von "LED_on" brät, wird das
nichts.
Johann L. hatte vor kurzem so etwas hier gepostet, ich finds bloß nicht
mehr. Aber es geht auf jeden Fall (ich glaube es waren 3 oder 4 Makros
von Nöten)
also ohne die makefile-Version von Rolf Magnus
PittyJ schrieb:> Ich verzichte fast komplett auf den Präprozessor.> Die Makros führten nur dazu, dass der Code schwerer zu verstehen und zu> warten ist.Axel Schwenke schrieb:> Persönlich glaube ich, daß Aussagen der Art "der Präprozessor ist pöhse"> hauptsächlich von Leuten kommen, die den Präprozessor nicht verstehen> und (deshalb) Angst davor haben.
QED.