Abend,
mir fällt immer öfter auf, dass in Code von (vermeintlichen?) Profis oft
seltsame Berechnungen einer simplen if-Abfrage vorgezogen werden.
Promintes Beispiel:
Ein Ringpuffer.
Wenn der Schreibindex den höchsten Wert erreicht hat muss er
zurückgesetzt werden.
Eleganter, aber auch (vor allem aufn uC) rechenintensiver:
1
buffer_write_index%=buffer_size;
Und letztens gesehn bei Code einer Stuttgarter Zulieferfirma für
Automotive-Software:
1
buffer_write_index&=mask;
(mit z.B. mask = 0x7F)
Wahrscheinlich schneller als die Modulodivision, aber erlaubt nur
Zweierpotenzen als Puffergrößen.
Gibt noch mehr solche Beispiele.
Was mich interessiert: Ist sowas irgendwie ein Zeichen von Können? Sind
if-Abfragen so verpönt?
Eine if-Abfrage kann auch ein Anfänger sofort verstehen. Will man das
bewusst verhindern? Will man zeigen dass man um Ecken denken kann? Und
was haben die Illuminaten damit zu tun?
Wenn ich Berechnungen einer If-Abfrage vorziehe, dann meistens deshalb,
weil ich das mathematische Konzept verstanden habe und nun in Software
umsetze.
Außerdem muss ich mir bei If-Abfragen überlegen, ob ich wirklich alle
möglichen Fälle abgedeckt habe oder ob noch irgendwo eine
Definitionslücke offen ist. Bei Berechnungen ergibt sich das oft von
selbst.
blubb schrieb:> buffer_write_index &= mask;
Diese Variante ist eindeutig die schnellste. In Assembler wird auch nur
ein Befehl für eine UND-Verknüpfung benötigt. Daher ist diese Variante
ideal für uCs.
Ich persönlich mache solche "Optimierungen" erst, wenn es wirklich nötig
ist und der Compiler es nicht schafft aus dieser if Abfrage eine
UND-Verknüpfung zu erstellen. Ansonsten schreibe ich meine Programme
möglichst einfach und verständlich.
lesbarer ansehen, weil dort genau steht, was man möchte. In der Regel
mache ich keine peephole-Optimierungen. Das überlasse ich dem Compiler,
es sei denn der Compiler schafft es nicht und es bringt tatsächlich
etwas.
IHMO Verpönnte if-Abfragen sind sinnfreie Abfragen
blubb schrieb:> Eine if-Abfrage kann auch ein Anfänger sofort verstehen. Will man das> bewusst verhindern? Will man zeigen dass man um Ecken denken kann? Und> was haben die Illuminaten damit zu tun?
Die meisten Programme werden nicht mit der Intention geschrieben, dass
jeder Anfänger sie es nachvollziehen kann, sondern dass man selber es
kann. Programme sind keine Demo-Plakate oder Eindruck schindende
Powerpoint-Sheets. Ausnahme: Beispielcode.
IF ist halt verknüpft mit Bedingungsprüfung und anschliessendem Sprung
(oder eben nicht). Das ist für CPU's mit Cache und Prefetch eher
schlecht (von der Performance her). Man kann nun generell versuchen
solche Sprünge zu vermeiden um die Anzahl Cache-Misses gering zu halten.
Ein anderer Effekt von Sprüngen ist auch eine schwankende Laufzeit. Bei
Sicherheitkritischen Systemen ist eine solche Variabilität eher
schlecht, da man so für den Beweis der korrekten Funktion mehr Aufwand
treffen muss.
Allerdings würde ich persönlich bei Systemen wo die beiden genannten
Kriterien nicht wirklich wichtig sind, das IF-Konstrukt vorziehen da es
einfach für MENSCH als Interpreter leichter lesbar ist. Bei Ringpuffern
würde ich allerdings immer die Zweierpotenz als Grösse wählen, das ist
schnell und trotzdem leicht lesbar.
Statt einfachem IF ist allerdings bei Schleifen generell WHILE
vorzuziehen, da sieht man sofort dass es eine Schleife ist. Der Compiler
macht ohnehin denselben Assemblercode daraus.
Grüsse, Reinhold
Reinhold_B schrieb:> IF ist halt verknüpft mit Bedingungsprüfung und anschliessendem Sprung> (oder eben nicht). Das ist für CPU's mit Cache und Prefetch eher> schlecht (von der Performance her).
Solche Überlegungen kann man allerdings oft den Compilern überlassen.
Die wissen das nämlich auch und sind ggf. in der Lage, sprungfreien Code
zu erzeugen.
> Man kann nun generell versuchen> solche Sprünge zu vermeiden um die Anzahl Cache-Misses gering zu halten.
Mit Caches hat das nichts zu tun. Aber mit der Pipeline-Tiefe und den
daraus resultierenden Kosten von Sprüngen oder falscher
Sprungvorhersage.
Beispiel: GCC für ARM(e) macht aus einer if-Abfrage
cmp r0, #99
movgt r0, #0
Das ist wesentlich billiger als eine dank Zweierpotenz auf eine Maske
reduzierte Modulo-Operation, bei der der Programmierer vergessen hat,
einen vorzeichenlosen Datentyp zu verwenden. Und sehr viel billiger,
wenn sein Nachfolger den Puffer von 256 auf 500 Bytes vergrössert und
dabei nicht an solche Schweinereien denkt.
Thorsten schrieb:> Und vorallem ist:>> if (buffer_write_index >= buffer_size) buffer_write_index = 0;>> semantisch nicht äquivalent zu:>> buffer_write_index %= buffer_size;
Das da. Das ist der wichtigste Grund.
Bei einem einfachen Ringpuffer kann es eigentlich keinen Unterschied
zwischen beiden Varianten geben, aber "can't happen" passiert eben doch
manchmal.
Der erfahrene Programmierer mag es eben etwas defensiv. Wenn
buffer_write_index aus irgendeinem Grund echt größer als buffer_size
ist, dann setzt die if-Abfrage den Index auf 0, während die
arithmetische Methode den Index auf Differenz zwischen Puffergröße und
altem Index.
Als konkretes Zahlenbeispiel:
Sei buffer_size = 16 und buffer_write_index = 17 (warum auch immer, z.B.
ein Programmierfehler in einer Interruptroutine). Die if-Abfrage setzt
den Index auf 0, die Arithmetik auf 1. Damit bleibt bei der
Arithmetik-Variante wenigstens die Anzahl der gepufferten Bytes korrekt.
Die gepufferten Daten sind in jedem Fall korrumpiert, aber die
Kommunikation kann in der zweiten Variante leichter gerettet werden.
Das ist ein wenig wie die Frage, welche der beiden folgenden If-Abfragen
man nehmen sollte:
In einem korrekt funktionierenden Programm dürfte es eigentlich keinen
Unterschied geben, aber jedes Programm enthält Fehler(*). Darum nimmt
man eben die zweite Variante. Und als Weiterführung des
Robustheitsgedanken nehmen erfahrenere Programmierer gerne arithmetische
Berechnungen.
Ein weiterer Vorteil von Berechnung über Verzweigungen ist, dass der
Programmfluss linear bleibt, was die Analyse vereinfacht. Das mag bei
einem Einzeiler unnötig aussehen, aber wenn man dann irgendwann 4
if-Abfragen verschachtelt hat, wird es anspruchsvoll.
(*) Daraus ergibt sich übrigens ein interessanter Zusammenhang:
Es ist ja allgemein anerkannt, dass man jedes Programm immer um
wenigstens eine Instruktion kürzen kann, ohne dass sich die Funktion
wesentlich ändert. Wenn aber jedes Programm mindestens einen Fehler
enthält, dann ergibt eine induktive Schlussfolgerung aus diesen beiden
Annahmen, dass jedes Programm eigentlich nur aus einer einzigen
Instruktion besteht, und die ist auch noch fehlerhaft.
Das erklärt eine Menge über die Software, mit der wir uns tagtäglich
rumplagen ;)
Sam P. schrieb:> Bei einem einfachen Ringpuffer kann es eigentlich keinen Unterschied> zwischen beiden Varianten geben,
Wenn man davon absieht, dass Modulo-Operationen schweineteuer sein
können und dieses Bisschen vorsorgliche Optimierung daher sehr wohl
angebracht sein kann. Puffergrössen können sich auch mal ändern.
Wenn man also Modulo verwendet, dann sollte man dies im Auge behalten
und dort, wo die Puffergrösse definiert wird, lautstark darauf
hinweisen, dass es eine Zweierpotenz sein muss. Und der Nachfolger des
ursprünglichen Programmierers wird unschöne Worte über seinen Vorgänger
finden, wenn zwar noch 100 Bytes im RAM frei sind, aber dieser Vollhorst
aufgrund seiner Programmiertechnik dessen Nutzung unmöglich gemacht hat.
A. K. schrieb:> Beispiel: GCC für ARM(e) macht aus einer if-Abfrage> cmp r0, #99> movgt r0, #0
Und das ist besser als eine einzelne AND-Instruktion? Abgesehen davon,
dass das egal ist, weil man auch auf 'nem µC heutzutage normalerweise
keine so großen Performance-Probleme hat, die solche Handoptimierungen
rechtfertigen. Robust und wartbar Programmieren zahlen sich auch da weit
mehr aus.
Ausgenommen davon sind natürlich Programmteile, die nachweislich (d.h.
durch Profiling o.ä. gemessen) an der Leistungsgreze des Controllers
operieren. Da zählt selbstverständlich jeder Taktzyklus.
> Das ist wesentlich billiger als eine dank Zweierpotenz auf eine Maske> reduzierte Modulo-Operation, bei der der Programmierer vergessen hat,> einen vorzeichenlosen Datentyp zu verwenden. Und sehr viel billiger,
Das Argument geht leider genau in die andere Richtung: Gerade die
Bitmaske funktioniert mit und ohne Vorzeichen gleich, während ein
Überlauf ins Negative bei der if-Abfrage katastrophale Folgen hat.
> wenn sein Nachfolger den Puffer von 256 auf 500 Bytes vergrössert und> dabei nicht an solche Schweinereien denkt.
Das lässt sich bei geeigneter Wahl der Konstanten leicht verhindern.
Beispiel:
A. K. schrieb:> Die meisten Programme werden nicht mit der Intention geschrieben, dass> jeder Anfänger sie es nachvollziehen kann, sondern dass man selber es> kann.
Kommt drauf an ob privat oder beruflich.
In einem Software-Review würde ich mich z.B. nicht für irgendwelches
Bitgeschiebe und -verknüpfen rechtfertigen müssen was vielleicht
schneller ist, aber den Sinn des Codes erstmal verschleiert. (z.B. PeDas
Tastenentprellen).
Da ist Wartbarkeit und Verständlichkeit ein sehr wichtiger Faktor.
Ja, die Maskierung ist schnell, aber bringt eben nicht nur Vorteile.
Finde es nur teilweise interessant sowas in freier Wildbahn zu finden.
Kennt ihr noch mehr so Beispiele?
A. K. schrieb:> Wenn man davon absieht, dass Modulo-Operationen schweineteuer sein> können und dieses Bisschen vorsorgliche Optimierung daher sehr wohl> angebracht sein kann. Puffergrössen können sich auch mal ändern.
Ja, ich beziehe mich ausschließlich auf Zweierpotenzen. Einen beliebigen
Modulo würde ich einem µC nicht zumuten wollen, da bin ich ganz deiner
Meinung. Auf einem ARM mit Hardware-Divide könnte das anders aussehen,
aber da habe ich mich nie mit befasst. Zweierpotenzen haben für mich
bisher gut gepasst, insbesonder da Puffergrößen ohnehin immer etwas
Willkür in sich haben.
Sam P. schrieb:> Und das ist besser als eine einzelne AND-Instruktion?
Das habe ich nicht geschrieben. Es ist macht aber grad mal einen Befehl
aus und die if-Variante ist im Unterschied zu den Alternativen völlig
unabhängig davon, ob es eine Zweierpotenz ist, und funktioniert auch
dann effizient, wenn die Grösse keine Konstante ist.
> dass das egal ist, weil man auch auf 'nem µC heutzutage normalerweise> keine so großen Performance-Probleme hat, die solche Handoptimierungen> rechtfertigen.
Ausnahme: Es muss für eine Modulo-Operation wirklich dividiert werden.
> Robust und wartbar Programmieren zahlen sich auch da weit> mehr aus.
Eben. Die if-Variante ist in diesem Sinn robuster als die Alternativen
und fast so billig wie die optimale Variante.
Sam P. schrieb:> Auf einem ARM mit Hardware-Divide könnte das anders aussehen,
Nicht wirklich. Es ist nur nicht ganz so schlimm wie ohne Divider. Im
Unterschied zur Multiplikation, die in Hardware parallelisierbar ist und
dann nur sehr wenige Takte kostet, ist die Division bei Prozessoren auch
in Hardware stets sequentiell, wenn auch u.U. mit 2 Bits pro Takt.
A. K. schrieb:> Wenn man davon absieht, dass Modulo-Operationen schweineteuer sein> könnenKönnen, aber nicht müssen. Wenn man beispielsweise Puffergrößen als
Zweierpotenzen definiert, dann nicht.
Sam P. schrieb:> Das ist ein wenig wie die Frage, welche der beiden folgenden If-Abfragen> man nehmen sollte:> if (buffer_write_index == buffer_size) buffer_write_index = 0;>> oder> if (buffer_write_index >= buffer_size) buffer_write_index = 0;
Wenn es äquivalent zum Modulo sein soll, die Division aber unerwünscht
ist, bleibt eigentlich nur diese Variante:
A. K. schrieb:>> Robust und wartbar Programmieren zahlen sich auch da weit>> mehr aus.>> Eben. Die if-Variante ist in diesem Sinn robuster als die Alternativen> und fast so billig wie die optimale Variante.
Wie ich schon beschrieben habe, hat eine if-Abfrage nicht zu
vernachlässigende Probleme. Das mit dem Vorzeichen, auf das du mich
gebracht hast, halte ich sogar für das schlimmere Problem, da sowas
immer wieder passiert.
Dennoch gibt es natürlich auch gute Gründe für diese Variante. Kann die
Puffergröße keine Zweierpotenz sein, dann ist Arithmetik problematisch.
Ist sie nicht konstant, gebe ich dir auch recht. Aber der Fall
buffer_write_index > buffer_size ist ein echtes Problem. Für die gleiche
Robustheit müsste es genau genommen so lauten:
Das hat fast alle Vorteile der Modulo-Rechnung, insbesondere die
korrekte Erhaltung der Pufferposition, vermeidet aber für den weit
überwiegenden Fall der normalen Programmfunktion die Modulus-Berechnung.
Es bleibt nur noch das Vorzeichenproblem, das auch die einfache
if-Abfrage hat. Und die Verwendung von "while" ist auch nicht gerade
intuitiv verständlich.
Dass solche Integer-Überlauf- bzw. Vorzeichenprobleme relevant sind,
zeigen etliche Sicherheitslücken. Ein Klassiker ist z.B. der
Apache-Webserver gewesen, der vor ein paar Jahren beim Header-Parsen in
genau so eine Falle getappt ist. Mit einer Bitmaske wäre das nicht
passiert.
Sam P. schrieb:> while (buffer_write_index >= buffer_size) buffer_write_index -= buffer_size;
Ja nee, is klar... Und was glaubt ihr wohl wie ein Modulo berechnet
wird? Nur weil das jetzt händisch nach programmiert ist, wird es noch
lange nicht schneller als ein %
Das ist für mich etwas holprig zu lesen, funktioniert aber für beliebige
Werte von buffer_size und braucht keine rechenaufwendige Division.
1
buffer_write_index%=buffer_size;
Da ist für mich sofort klar, was passiert. Allerdings wird der Compiler
eine Division generieren, wenn der Wert von buffer_size zur Compile-Zeit
nicht bekannt oder keine Zweierpotenz ist. Das ist nicht so schön.
1
buffer_write_index&=mask;
Da muss ich erst etwas nachdenken, wenn ich nicht weiß, worum es dabei
geht. mask könnte ja irgendeinen krummen Wert, bspw. 11011001₂ haben,
denn man sieht nicht sofort, dass mask in einer speziellen Beziehung zu
buffer_size steht. Zudem funktioniert die Methode nur, wenn buffer_size
eine Zweierpotenz ist. Dafür ist sie sehr schnell, da sie nicht nur ohne
Division, sondern auch ohne Sprünge auskommt.
Noch etwas zur Lesbarkeit:
Deinen drei Beispielen geht ziemlich sicher noch eine Erhöhung von
buffer_write_index um 1 voran, also etwa so:
Um zu erfassen, dass dieser Codeabschnitt einfach nur umbrechend
hochzählt, brauche ich 7 Sekunden, wenn ich den Code zum ersten Mal
lese. Erschwert wird das Ganze noch dadurch, dass 4 Variablennamen
auftreten, die alle mit "buffer_" beginnen. Da muss ich erst einmal
schauen, welche davon gleich sind.
Die zweite Variante könnte so aussehen:
Hier treten nur 3 Variablennamen auf, und "+1" und "%x" sind Muster, die
mein Gehirn sofort in eine andere Ausdrucksweise umsetzt:
+1 -> Zähle hoch
%x (im Kontext einer vorangegangenen Addition) -> breche um bei x
Tatsächlich passiert diese Umsetzung sogar in einem einzelnen Schritt,
also:
+1 %x -> zähle bei x umbrechend hoch
Hier dauert das Lesen und Verstehen nur 4 Sekunden. Einen Großteil der
Zeit verschlingt der Vergleich der langen Variablennamen (die ich hier
aber nicht kritisieren möchte). Stände da bspw. nur
1
wi=(wi+1)%bsize;
würde das die Erfassungszeit auf 2 Sekunden reduzieren.
Solche Dinge wie das umbrechende Hochzählen sind für den Computer zwar
zwei bis drei Operationen, für meinen Kopf aber nur eine einzige.
Deswegen mag ich es, wenn man den gesamten Ausdruck in eine einzige,
möglichst kurze Zeile schreiben kann. Diese nehme ich dann weniger als
Text, sondern eher als Bild wahr, was deutlich schneller vonstatten geht
(wobei ich die Variablennamen natürlich immer noch als Text lesen muss).
Diese Abstraktion auf etwas Kurzes ist ja auch einer der Gründe, warum
man längere Codeabschnitte, die eine logische Einheit bilden, zu einer
Funktion zusammenfasst, so dass man die Ausführung des gesamten Codes
auf eine einzige Zeile, nämlich den Funktionsaufruf reduzieren kann.
Trotz der für mich besseren Lesbarkeit der Modulo-Methode setze ich
diese aber so gut wie nie ein. Warum?
Da solche FIFO-Geschichten oft in Interrupt-Handlern ausgeführt werden,
hat ausnahmsweise Effizienz eine höhere Priorität als die Lesbarkeit des
Codes. Da die Maskierungsmethode voraussetzt, dass die Puffergröße eine
Zweierpotenz ist, gebe ich meist der if-Abfrage den Vorzug. Das sieht
dann etwa so aus:
1
if(++buffer_write_index==buffer_size)
2
buffer_write_index=0;
Durch die Übernahme der Inkrementoperation in die if-Bedingung wird die
Anzahl der Variablennamen auf 3 reduziert, von denen ich die beiden
buffer_write_index direkt untereinander schreiben kann, so dass man
sofort erkennen kann, dass es sich tatsächlich um die gleiche Variable
handelt.
Das >= habe ich übrigens durch ein == ersetzt, da der inkrementierte
buffer_write_index nie größer als buffer_size werden kann. Sollte dieser
Fall doch eintreten, ist das ein Fehler. Durch die Verwendung von ==
anstelle von >= tritt dieser Fehler viel deutlicher (also schon in einer
frühen Projektphase) in Erscheinung, weil dadurch ein größerer Teil des
RAMs zugemüllt wird.
Und weil ich das in den Interrupt-Handlern so mache, wende ich die
if-Abfrage natürlich auch in normalen Programmteilen an, damit wieder
alles schön einheitlich ist.
Wenn die FIFO-Operationen noch zeitkritischer sind und man es sich
RAM-mäßig leisten kann, den Puffer 256 Bytes groß zu machen, kann man
das umbrechende Hochzählen auch einfach so realisieren:
1
buffer_write_index++;// Variable ist vom Typ uint8_t
Der Umbruch geschieht dann ohne Zeitverlust automatisch in der Hardware.
Klaus schrieb:> Ja nee, is klar... Und was glaubt ihr wohl wie ein Modulo berechnet> wird? Nur weil das jetzt händisch nach programmiert ist, wird es noch> lange nicht schneller als ein %
Ich halte den Algorithmus für den schlechtestmöglichen
Modulo-Algorithmus, daher gehe ich davon aus, dass eine optimierte
Runtime-Funktion das besser macht.
Darum geht es aber gar nicht.
Diese Variante ist für den häufigsten Fall, nämlich dass
buffer_write_index < buffer_size, so schnell wie es mit der if-Variante
nur möglich ist. Und für den zweithäufigsten Fall, nämlich dass
buffer_write_index == buffer_size, ist das immer noch so gut wie
optimal. Das ist das wichtigste. Dass das dann im Fehlerfall langsam ist
und keinen Vorteil gegenüber einer direkten Modulo-Rechnung hat, das ist
irrelevant, denn in dem Fall geht es nur noch darum, zu Retten was noch
zu Retten ist. Stichwort "graceful degradation".
Yalu X. schrieb:> Erschwert wird das Ganze noch dadurch, dass 4 Variablennamen> auftreten, die alle mit "buffer_" beginnen. Da muss ich erst einmal> schauen, welche davon gleich sind.
Weshalb man etwas aufpassen sollte, wie man Variablen benennt. In C
neigt man traditionell dazu, komplexere Namen zu verwenden, um darin die
Modularisierung globaler Variablen auszudrücken. Das geht zwar auch
anders, aber so ist oft der Stil. In C++ ergibt sich der natürliche Stil
weit eleganter, die Namen sind kompakter und damit schneller erfassbar.
Yalu X. schrieb:> if (++buffer_write_index == buffer_size)> buffer_write_index = 0;
Das finde ich auch nicht schlecht. Ursache und Wirkung sind so nahe
beieinander, wie es nur geht.
Ich bleibe aber grundsätzlich misstrauisch. Leider neigt man ja als
Programmierer (mich eingeschlossen) dazu, zu glauben, man hat alles
bedacht, und hinterher fummelt da doch noch jemand anderes an
buffer_write_index rum, sei es weil man da noch eine Zeile
übersehen/vergessen hat, wegen eines "davongelaufenen" Zeigers, oder gar
weil man von Außen eingegebene Werte ohne Validierung verwendet hat
(Todsünde!).
"==" würde ich an der Stelle grundsätzlich niemals verwenden. ">=" ist
das absolute Minimum, insbesondere da es genau gar nichts an Codelänge
oder Ausführungszeit ändert. Darüber hinaus halt Zweierpotenz und
Bitmaske, wurde ja alles schon durchgekaut.
> buffer_write_index &= mask;>> Da muss ich erst etwas nachdenken, wenn ich nicht weiß, worum es dabei> geht. mask könnte ja irgendeinen krummen Wert, bspw. 11011001₂ haben,> denn man sieht nicht sofort, dass mask in einer speziellen Beziehung zu
Das ist einer der Fälle, wo die Erfahrung als Programmierer viel von dem
beeinflusst, was man als "offensichtlich" oder "naheliegend" empfindet.
Das lässt sich schwer pauschalisieren, auch wenn für ich das Maskieren
als Basistechnik empfinde.
Wer es expliziter will, versteckt die Maske halt nicht, auch wenn es
mehr zu tippen ist:
buffer_write_index &= (1<<BUFFER_SIZE_EXP)-1;
So ist dann die spezielle Beziehung deutlich gemacht. Wenn es um
selbstdokumentierenden Code geht, kann man das aber noch leichter mit
einer (inline-)Funktion erreichen und ist dann sogar flexibel genug für
einen Wechsel des Verfahrens:
buffer_write_index = wrap_index(buffer_write_index, buffer_size);
> buffer_size steht. Zudem funktioniert die Methode nur, wenn buffer_size> eine Zweierpotenz ist. Dafür ist sie sehr schnell, da sie nicht nur ohne> Division, sondern auch ohne Sprünge auskommt.
Viele Prozessoren haben heute Instruktionen mit bedingter Ausführung
(klassisches ARM sogar für alle vorhandenen Instruktionen). Maximal geht
halt ein Taktzyklus wegen eines nicht ausgeführten Befehls flöten, aber
die zu Recht gefürchteten pipeline flushes bleiben auf jeden Fall aus.
Sam P. schrieb:> buffer_write_index = wrap_index(buffer_write_index, buffer_size);
Nur mal am Rande, für diejenigen, die GCC's Optimierungsfähigkeiten
nicht kennen:
1
intwrap_index(intindex,intsize){
2
if((size-1)&size==0){
3
returnindex&(size-1)
4
}else{
5
while(index>=size)index-=size;
6
returnindex;
7
}
8
}
So könnte die Funktion aussehen. Im ersten Moment könnte man denken,
dass das hoffnungslos überkompliziert ist.
Vorausgesetzt, size ist GCC wahrend der Kompilierung als Konstant
bekannt, dann wird GCC diese Funktion jedoch inlinen und automatisch und
korrekt Bitmaskierung oder while-Schleife wählen. Mit etwas mehr
Trickserei kann man dann auch noch den nicht-konstanten Fall optimal
behandeln lassen.
Es gibt also keinen technischen Grund, weniger lesbaren oder weniger
robusten Code zu schreiben.
Damit wird es aber langsam wirklich off-topic. Mit der Ursprungsfrage
hat das bald nichts mehr zu tun.
> Ist sowas irgendwie ein Zeichen von Können?
Unter Umständen ja. Vielleicht aber auch Verschleierung oder Coolness
:-)
> Sind if-Abfragen so verpönt?
Hängt vom Umfeld ab. Absolut gesprochen ja, da es meist eine bessere
funktionale Alternative gibt. D. h. der ganze Kram mit "mutable
variables/arrays", "if", "switch", "NULL", das ganze imperative Zeugs
mit seinen "side effects", das kommt langsam aus der Mode, zumindest in
der Schickeria (die LISP-Fraktion würde sagen, dass sie das schon immer
so gemacht hat).
Konkret für einenen "Ringpuffer" würde ich das in Scala so formulieren,
Beispiele für "mit ohne if"
1
class LimitedFifo[T](val elements: List[T], val limit: Int) extends Immutable {
2
3
/**
4
* Add an element to the FIFO.
5
*
6
* @return The new instance of the FIFO
7
*/
8
def +(newElement: T): LimitedFifo[T] = {
9
// drop() removes the first n elements, and is not confused by negative values
10
val newInstance = new LimitedFifo(elements.drop(elements.size + 1 - limit) :+ newElement, limit)
11
12
{
13
newInstance
14
} ensuring ({ size <= newInstance.size }) // Post condition
15
}
16
17
/**
18
* @return Element at given index
19
*/
20
def apply(index: Int) = {
21
// Pre conditions
22
require({ index >= 0 }, "Negative index: " + index)
23
require({ index < limit }, "Invalid index: " + index)
24
require({ elements.size > 0 }, "No elements");
25
26
elements(index)
27
}
28
29
[...]
In diesem Beispiel gibt es für alle "if statements" einen funktionalen
Ersatz: Einmal durch drop(), pre- und post conditions durch require()
bzw ensure().
Über das drop() an dieser Stelle kann man streiten, dennoch ist der
funktionale Stil elegant, weil bei dieser Art zu Entwickeln sehr
kompakter und aussagekräftiger Code entsteht - die Fehlerquote sinkt.
Im µC-Umfeld mit knappem RAM ist das nicht immer möglich, elua steht auf
meiner Liste :-)
Tom schrieb:> Außerdem muss ich mir bei If-Abfragen überlegen, ob ich wirklich alle> möglichen Fälle abgedeckt habe oder ob noch irgendwo eine> Definitionslücke offen ist. Bei Berechnungen ergibt sich das oft von> selbst.
Ja, das ist der funktionale Ansatz!
Sam P. schrieb:> "==" würde ich an der Stelle grundsätzlich niemals verwenden. ">=" ist> das absolute Minimum, insbesondere da es genau gar nichts an Codelänge> oder Ausführungszeit ändert.
">=" zeugt einfach davon, dass man seinem eigenen Code nicht traut,
zeigt also eine Verunsicherung... nach dem Motto: Wenn ich irgendwo
einen Fehler gemacht habe, so dass der Index größer wird als gewünscht,
so bügle ich das hier wieder aus.
Ich gebe aber zu, dass so ein Handeln absolut menschlich ist und dass
mir das auch manchmal beim Programmieren passiert ;-)
Frank M. schrieb:> ">=" zeugt einfach davon, dass man seinem eigenen Code nicht traut,> zeigt also eine Verunsicherung...
Nö, defensives Programmieren ist im Gegenteil sicherer.
Wenn der Index in den Wald zeigt, zerschießt der mir was ganz anderes
und ich suche ewig an der falsch Stelle.
Wenn der Index aber nur falsch ist, ist nur das Array zerstört, worauf
er zeigen soll und der Fehler läßt sich schnell eingrenzen.
Auch muß ich ja nicht selber den Fehler gemacht haben. Es kann ein
anderer am Projekt beteiligter oder auch eine Lib sein.
Deswegen arbeite ich auch bevorzugt mit Indexübergabe anstatt Pointern.
Den Index kann ich schnell prüfen ob er innerhalb sizeof(Array) ist, den
Pointer aber nicht.
Peter
Sam P. schrieb:>> Nur mal am Rande, für diejenigen, die GCC's Optimierungsfähigkeiten> nicht kennen:
Interessiert eigentlich gar nicht. Nicht alles wird mit dem GCC gemacht
und wer portablen schnellen Code schreiben will der verläßt sich nicht
auf so etwas, sondern schreib es gleich richtig, dann läuft es auch
richtig. Sorry aber das Beispiel ist einfach nur grauenhaft!
>> Es gibt also keinen technischen Grund, weniger lesbaren oder weniger> robusten Code zu schreiben.
Geht weniger lesbar als Dein Beispiel überhaupt?
So ein Code würde ich sofort durch vernünftigen lesbaren ersetzten.
Peter Dannegger schrieb:> Frank M. schrieb:>> ">=" zeugt einfach davon, dass man seinem eigenen Code nicht traut,>> zeigt also eine Verunsicherung...>> Nö, defensives Programmieren ist im Gegenteil sicherer.
Mit dem Vorschlag, ein == anstelle des >= zu verwenden, wollte ich
eigentlich eigentlich keine Diskussion auslösen. Da diese nun aber noch
angelaufen ist, möchte ich noch ein paar Worte zu dem Thema loswerden
(die ich auch oben schon kurz erwähnt habe):
Ich habe nichts gegen defensives Programmieren. Allerdings führt falsch
verstandenes defensives Programmieren meist nicht zur Vermeidung von
Fehlern, sondern nur zu deren Verdeckung. Der obige Code-Ausschnitt,
der wohl aus einer FIFO-Routine stammt, ist ein gutes Beispiel hierfür.
Solange das Programm fehlerfrei ist, der Index sich also immer innerhalb
der Array-Grenzen bewegt, ist es völlig egal, ob man == oder >= verwen-
det. Wird auf Grund eines Programmierfehlers buffer_write_index nach
seiner Inkrementierung echt größer als buffer_size, bedeutet das, dass
er schon vorher größer als buffer_size-1 war. Der letzte Wert, der in
die FIFO geschrieben wurde, landete also nicht in dem dafür vorgesehenen
Puffer, sondern irgendwo weiter hinten im Speicher.
Verwendet man >=, steht buffer_write_index beim nächten FIFO-Zugriff
wieder auf 0, also einem gültigen Wert, und alles scheint wieder in
Ordnung zu sein. Ist es aber nicht: Zum einen wurde ein Byte irgendwo im
Speicher fälschlicherweise überschrieben, zum anderen fehlt ein Zeichen
in der FIFO. Das einzelne, überschriebene Byte stört vielleicht nieman-
den, weil dort keine wichtigen Daten liegen. Das fehlende Zeichen in der
FIFO fällt vielleicht auch nicht auf, weil die über die FIFO gesendeten
Daten nicht maschinell weiterverarbeitet werden, sondern nur der Benut-
zerinformation (Terminalausgabe, Logdatei o.ä.) dienen. Das >= führt
somit dazu, dass das Programm vermeintlich fehlerfrei ist, obwohl es
einen ernsthaften Fehler enthält, der irgendwann später beim Kunden
Probleme macht.
Verwendet man hingegen ==, wird durch den zu großen Index nicht nur ein
einzelnes Byte, sondern gleich ein ganzer Speicherbereich überschrieben.
Entsprechend viele Zeichen fehlen in der FIFO und damit in den gesende-
ten Daten. Dieses, im Vegleich mit der >=-Methode sehr viel auffälligere
Fehlvehalten wird sehr zeitnah entdeckt werden und gar nicht erst den
Weg zum Kunden finden.
Der einzige Fall, wo ich dem >= den Vorzug geben würde, ist, wenn dieses
Bestandteil einer Softwareänderung ist, die kurz vor der Auslieferung
der Software durchgeführt wird, so dass keine Zeit für intensive Tests
mehr bleibt. Dann ist es besser, den Schaden im Fehlerfall möglichst
gering zu halten ;-)
> Wenn der Index in den Wald zeigt, zerschießt der mir was ganz anderes> und ich suche ewig an der falsch Stelle.
Der falsche Index wird dir im genannten Beispiel praktisch immer etwas
ganz anderes zerschießen.
> Wenn der Index aber nur falsch ist, ist nur das Array zerstört, worauf> er zeigen soll und der Fehler läßt sich schnell eingrenzen.
Wenn sich der Index innerhalb der Array-Grenzen befindet, spielt es
keine Rolle, ob du == oder >= verwendest.
Unter echtem defensiven Programmieren verstehe ich den Verzicht auf
effiziente, aber komplizierte Programm- und Datenkonstrukte zugunsten
eines weniger effizienten, dafür aber leichter zu durchschauenden und
damit weniger fehlerträchtigen Stils.
Im obigen Beispiel wäre ein einfacher Fall von defensiver Programmierung
die Verwendung der If-Abfrage anstelle der Maskierungmethode.
Yalu X. schrieb:> Ich habe nichts gegen defensives Programmieren. Allerdings führt falsch> verstandenes defensives Programmieren meist nicht zur Vermeidung von> Fehlern, sondern nur zu deren Verdeckung.
Sehe ich auch so, wobei man hier zwei "Verbesserungsmöglichkeiten hat:
Index vor dem schreiben prüfen ob innerhalb der Werte und bei einem
Fehler diesen irgendwie mitteilen/Reset/...
Roland H. schrieb:> Hängt vom Umfeld ab.Roland H. schrieb:> Konkret für einenen "Ringpuffer" würde ich das in Scala so formulieren,
Das Umfeld ist das Forum "gcc" auf "mikrocontroller.net"...
Oliver
Läubi .. schrieb:> Index vor dem schreiben prüfen ob innerhalb der Werte und bei einem> Fehler diesen irgendwie mitteilen/Reset/...
So hatte ich das auch gemeint. Nach dem Schreiben ist es wirklich
wurscht ob man auf == oder >= prüft.
Den Index durch Maskierung zu ermitteln, bringt keine merkbare
Einsparung, dafür aber erhebliche Einschränkungen.
Wenn ich z.B. eine Flash-Page (256 Byte) mit CRC16 absichern will,
brauche ich 258 Byte Puffer. Bei Maskierung müßte ich 512 Byte nehmen,
also 254 Byte RAM verschenken.
Peter
Yalu X. schrieb:> Ich habe nichts gegen defensives Programmieren. Allerdings führt falsch> verstandenes defensives Programmieren meist nicht zur Vermeidung von> Fehlern, sondern nur zu deren Verdeckung.
Full ACK.
> Unter echtem defensiven Programmieren verstehe ich den Verzicht auf> effiziente, aber komplizierte Programm- und Datenkonstrukte zugunsten> eines weniger effizienten, dafür aber leichter zu durchschauenden und> damit weniger fehlerträchtigen Stils.
Genau so sehe ich das auch. Unter defensivem Programmieren verstehe
ich insbesondere das portable Programmieren, keinesfalls aber die
Verdeckung von Fehlern.
Danke für Dein Statement dazu :-)
> Genau so sehe ich das auch. Unter defensivem Programmieren verstehe> ich insbesondere das portable Programmieren, keinesfalls aber die> Verdeckung von Fehlern.
Yep.
Ich muss nur an eines meiner "Lieblingsmuster" in fremdem Code denken
(viel zu oft gesehen):
Oliver S. schrieb:>> Konkret für einenen "Ringpuffer" würde ich das in Scala so formulieren,>> Das Umfeld ist das Forum "gcc" auf "mikrocontroller.net"...
Ja und?
Der Titel des Threads ist allgemein, und meine Antwort darauf lautet
"Ja". Sieht man schön hier:
der mechatroniker schrieb:> // das wars, kein else
Im Übrigen lassen sich viele Konzepte (Design by contract, funktionale
Programmierung, immutable references/variables, OOP, ...) aus anderen
Bereichen sehr wohl in der richtigen Dosis in den µC-Bereich übertragen.
Wenn aber der Zielhafen nicht bekannt ist, dann ist jeder Wind
ungünstig.
Hier meine (stark gekürzte) Implementierung des erweiterten Themas, in
der ARM-Variante:
Hier sind der optimistische Ansatz mit "==" enthalten,
und der pessimistische Ansatz mit Prüfung ebenso.
Das letzere lässt sich durch #defines nach der Entwicklungs- und
Testphase ggf. zentral abschalten, so man den Code in weniger Flash
pressen will.
Frank M. schrieb:> ">=" zeugt einfach davon, dass man seinem eigenen Code nicht traut,> zeigt also eine Verunsicherung... nach dem Motto: Wenn ich irgendwo> einen Fehler gemacht habe, so dass der Index größer wird als gewünscht,> so bügle ich das hier wieder aus.
Dem stimme ich zu. ">=" ist gut gemeint, aber das Problem wird
verschleiert, ggf. verzögert, und macht sich vor allem nicht direkt
bemerkbar.
Den letzeren Aspekt finde ich wichtig. Im Beispiel steht stellvertretend
für das "sich bemerkbar machen" eine ARM-Variante: Es wird ein SW
breakpoint erzeugt, ohne Debugger ergibt das einen "hard fault", und der
"hard fault trace" zeigt im PC die Stelle ziemlich genau an. Im
Produktionsbuild könnte dies durch eine Endlosschleife der ein "CPU
reset" ersetzt werden etc.
Roland H. schrieb:> if (expression == false)
Je nun, ohne if gehts halt doch nicht. Das ist dann zwar durch etwas
Klassenmagie versteckt, aber am Ende steht es dann doch da...
Oliver
Oliver S. schrieb:> Je nun, ohne if gehts halt doch nicht. Das ist dann zwar durch etwas> Klassenmagie versteckt, aber am Ende steht es dann doch da...
Wenn Du das andere "if" findest, dann darfst Du es behalten :-)
Wer hat behauptet, dass es ohne geht? Die Frage war, ob es "verpönt"
ist.
Die Klassen sind hier nicht relevant, das ginge auch ohne; konkrete
Vorteile in dem Beispiel sind private Variablen im Buffer und um einen
"name space prefix" zu bekommen.
Das mit dem "Magier" ist korrekt, denn es verlagert das "if" eine Ebene
tiefer, wo eher die "Seniors" aktiv werden. Darauf aufbauend ist kein
"if" mehr nötig, deren Anzahl im Quelltext sinkt und die Semantik
steigt.
Es geht auch oft ohne "if" - das soll es illustrieren.
Man sollte immer das passende Werkzeug nehmen.
Wenn z.B. die Entprellroutine 8 Tasten entprellt und die Flags setzen
soll, ist ein OR über die 8 Bits natürlich viel effektiver und besser
lesbar, als eine Kette von 8 Ifs.
Und ein Switch ist wiederum besser lesbar, als ein Haufen Ifs über
dieselbe Variable.
Peter
der mechatroniker schrieb:>> Genau so sehe ich das auch. Unter defensivem Programmieren verstehe>> ich insbesondere das portable Programmieren, keinesfalls aber die>> Verdeckung von Fehlern.>> Yep.>> Ich muss nur an eines meiner "Lieblingsmuster" in fremdem Code denken> (viel zu oft gesehen):>>
1
>// ...
2
>if(somePointer){
3
>// ...
4
>tu_was_mit(somePointer->irgendwas);
5
>// ...
6
>}
7
>// das wars, kein else
8
>
Ok, du störst dich jetzt daran dass ein nicht leerer Pointer ja auf
einen falschen Bereich zeigen könnte. Das hat nicht viel mit einem
Defizit an defensiver Programmierung zu tun, das ist eher schon
Paranoid.
Mich würde interessieren, wie so ein Code ausschaut, nachdem du ihn
"verbessert" hast?
Er meint wahrscheinlich, man soll entweder die Abfrage weglassen (dann
merkt man beim Testen sofort, dass etwas nicht stimmt, wenn somePointer
wegen eines Fehlers Null ist) oder einen else-Zweig einführen, der dafür
sorgt, dass man den Fehler bemerkt.
So wie es da steht, stürzt das Programm bei einem Nullpointer zwar nicht
ab, aber der Fehler (das somePointer an der Stelle überhaupt Null ist)
besteht weiter und wird womöglich schwierig zu finden.
Anders sieht es natürlich aus, wenn es an der Stelle ausdrücklich im
normalen Betrieb vorgesehen ist, dass somePointer Null sein kann und in
dem Fall nichts gemacht werden soll. Muss man sich überlegen, ob das ein
sinnvolles Feature ist oder es nicht doch eher Fehler verdeckt.
Genau das meine ich. Eigentlich sollte somePointer an der Stelle nicht
null sein, allerdings wird eine Angst-if-Abfrage eingebaut, die, falls
woanders was schiefgelaufen ist, die NullPointerException (in
managed-Sprachen) oder das undefined behavior (in C[++]) verhindert. Das
dann allerdings mit dem Ergebnis, dass, wenn somePointer wirklich mal
null ist, ein Vorgang scheinbar erfolgreich durchläuft, das Programm
aber in Wirklichkeit irgendeinen Schritt ersatzlos ausgelassen hat.
In das fehlende else würde natürlich irgendeine Art von Fehlerrückgabe,
-ausgabe oder -protokollierung gehören.
der mechatroniker schrieb:> Genau das meine ich. Eigentlich sollte somePointer an der Stelle nicht> null sein,
Warum, ist das ein neues Gesetz das von Dir eingeführt wurde?
> allerdings wird eine Angst-if-Abfrage eingebaut, die, falls> woanders was schiefgelaufen ist, die NullPointerException
du must noch viel lernen...
>> In das fehlende else würde natürlich irgendeine Art von Fehlerrückgabe,> -ausgabe oder -protokollierung gehören.
1) bedeutet ein Nullpointer noch lange nicht das überhaupt ein Fehler
aufgetreten ist. Also ist auch keinerelei Fehlermeldung nötig.
2) Du solltest mal dadüber nachdenken ob ein Nullpointer nicht auch
einfach heissen kann, das gewünscht wird was bestimmtes einfach nicht
gemacht wird.
Sprich Pointer==NULL bedeutet es soll etwas nicht gemacht werden soll,
Pointer != NULL eine bestimmte Aktion wird gewünscht und die benötigten
Daten findet man über den Pointer. z.B. bei Listen wird das Ende mit
einem Nullpointer gekennzeichnet. Da ist nichts kaputt oder falsch, da
ist einfach das ende der Liste erreicht. .. es gibt noch viele andere
Beispiele.
Mikel M. schrieb:> 1) bedeutet ein Nullpointer noch lange nicht das überhaupt ein Fehler> aufgetreten ist. Also ist auch keinerelei Fehlermeldung nötig.
So isses.
Beispiel:
Eine Task schreibt was in einen Puffer und setzt den Zeiger darauf.
Eine andere Task schaut nach, ob was im Puffer ist, also der Zeiger
nicht 0 ist. Dann verarbeitet sie die Daten und setzt den Zeiger wieder
auf 0. Damit weiß die erste Task, daß sie nun die nächsten Daten bereit
stellen kann usw.
Peter
Geht aber nur dann, wenn 0 keine gültige Adresse ist. Es gibt Systeme,
da ist 0 eine gültige Adresse. AVR ist eines davon: 0 ist gültige
EEPROM- und Flash-Adresse und bei den XMEGAs auch gültige RAM-Adresse
(I/O).
Vlad Tepesch schrieb:> manchmal gehört es aber auch zum Interface, dass der Pointer NULL sein> kann.
Ja, dann sollte es im Interface auch so dokumentiert sein. Und man muss
hoffen, dass diese auch gelesen wird. Kann funktionieren, muss aber
nicht.
Viele aktuelle Sprachen bieten Alternativen, die diese Dinge
deutlicher hervorheben: Anstatt von NULL wird vom Aufrufenden
verlangt, eine Option zu übergeben. Es wird somit im Interface beim
Typ des Arguments kodiert, ob ein leerer Wert möglich ist oder nicht.
Beide Seite werden somit vom Compiler darauf hingewiesen.
Mikel M. schrieb:> z.B. bei Listen wird das Ende mit> einem Nullpointer gekennzeichnet.
Nicht im allgemeinen: Es gibt Systeme, in welchen typ-sichere
Singleton-Objekte verwendet werden, die nicht ins Nirwana zeigen.
In Scala gibt es nach wie vor NULL, zwecks Kompatibilität zu Java.
Abgesehen davon wird an dessen Stelle None verwendet. Repräsentiert
das gleiche, allerdings ist es ein Objekt, und hat diverse Methoden,
wie z. B. foreach. Das vermeidet elegant die genannte "if"-Abfragen.
Zitat von http://en.wikipedia.org/wiki/Tony_Hoare
###
I call it my billion-dollar mistake. It was the invention of the null
reference in 1965. At that time, I was designing the first comprehensive
type system for references in an object oriented language (ALGOL W). My
goal was to ensure that all use of references should be absolutely safe,
with checking performed automatically by the compiler. But I couldn't
resist the temptation to put in a null reference, simply because it was
so easy to implement. This has led to innumerable errors,
vulnerabilities, and system crashes, which have probably caused a
billion dollars of pain and damage in the last forty years.
###
Allgemeine Aussagen sind immer falsch. ;-)
Ich habe zum Beispiel Routinen, wo ich in C so etwas wie
objektorientiert programmiere. Das sieht dann zum Beispiel so aus
(item_t ist via typedef struct definiert
1
item_t*item_new(void)
2
{
3
item_tthis=malloc(sizeof(item_t));
4
5
if(this)
6
{
7
/* Daten initialisieren */
8
}
9
returnthis;
10
}
Wenn im Speicher kein Platz mehr ist, wird NULL zurückgegeben und die
aufrufende Routine muss das halt abfangen.
> Warum, ist das ein neues Gesetz das von Dir eingeführt wurde?
Weil ich von einem Fall rede, in dem es so ist. Sprich einem, in dem in
der Doku zur Funktionsschnittstelle stehen würde (Existenz ausreichender
Doku vorausgesetzt): Der Aufrufer hat sicherzustellen, dass der Member
somePointer auf ein initialisiertes foo-Objekt zeigt. Dass es andere
Fälle gibt, wo man legitimerweise auf einen Nullpointer stößt, habe ich
ja gar nicht infrage gestellt.
> du must noch viel lernen...
Falls du auf die NullPointerException anspielst: Keine Sorge, hab mich
gerade schon weitergebildet. Das Dereferenzieren eines Nullpointers
führt nicht immer zu undefined behavior (in C oder C++) und der
NullPointerException (Java): In C# heißt letztere nämlich (man beachte
den ausschlaggebenden Namensunterschied) NullReferenceException (C#)
führen. In der Praxis irrelevant, denn dank Code Completion muss man
nach "catch Null" meist nimmer viel weitertippen.
Was ich natürlich vergessen hab zu erwähnen, ist, dass es in den Fällen,
von denen ich rede, oft eine adäquate Lösung wäre, einfach mal auf den
Pointer zuzugreifen und die gegebenenfalls geworfene Exception einfach
an den Aufrufer durchfallen zu lassen.
Johann L. schrieb:> 0 ist gültige> EEPROM- und Flash-Adresse und bei den XMEGAs auch gültige RAM-Adresse
Dieses Problem hatte ich auch mal mit einem 8051 Programm.
Es wurde einfach durch einen Linkerschalter gelöst:
XD (0001H)
D.h. ein Byte des XDATA wurde verschwendet.
Das Projekt habe ich aber schon so übernommen.
Peter
Dazu muss eigentlich nichts verschwendet werden. Nur weil ein Zeiger auf
die physikalisch erste Speicherstelle (mit der Adresse '0') zeigt, muss
er nicht automatisch auch '== 0' sein.
Sven P. schrieb:> Dazu muss eigentlich nichts verschwendet werden.
Welche andere Lösung schlägst du für Peters Problem mit dem 8051 vor?
> Nur weil ein Zeiger auf die physikalisch erste Speicherstelle (mit der> Adresse '0') zeigt, muss er nicht automatisch auch '== 0' sein.
Ja, an könnte einen C-Compiler schreiben, der in Pointer-Variablen immer
die Adresse plus 1 speichert. Bei jeder Dereferenzierung müsste dann
aber die 1 wieder subtrahiert werden, was auf dem alten 8-Bit-Teil
merklich Rechenzeit kosten würde. Zudem wäre durch dieses Ummappen an
einer anderen Stelle des Adressraums (nämlich an der Adresse 0xffff) ein
Byte nicht für C-Variablen verfügbar.
Da erscheint mir der Verzicht auf ein RAM-Byte die praktikablere Lösung
zu sein.
Yalu X. schrieb:> Sven P. schrieb:>> Dazu muss eigentlich nichts verschwendet werden.>> Welche andere Lösung schlägst du für Peters Problem mit dem 8051 vor?> [...]
Kommt auf die Implementierung an. Sicherlich ist der Verzicht auf eine
einzige Speicherstelle meistens am praktikabelsten, keine Frage.
Ich wollte nur verdeutlichen, dass es nicht sein muss. Eben, ein
festes Offset oder die Adressen verbreitern und oben ein Flag setzen und
so weiter, das ginge alles. Ich ärgere mich jedes Mal wieder, wenn Leute
die Ganzzahl-Darstellung eines Zeigers in C mit einer physikalischen
Speicheradresse gleichsetzen. Das ist nirgendwo so spezifiziert und
endet nicht selten in einem Unfall...
Sven P. schrieb:> Ich ärgere mich jedes Mal wieder, wenn Leute die Ganzzahl-Darstellung> eines Zeigers in C mit einer physikalischen Speicheradresse> gleichsetzen.
Beispiele, wo Pointer keine physikalischen Adressen sind, sind ja alle
Rechner mit virtueller Speicherverwaltung.
Oben (und auch in früheren Posts) hast du aber das "physikalisch"
weggelassen:
> Da wären wir wieder beim Thema, dass Zeiger keine Adressen sind.
Wolltest du damit nur ausdrücken, dass der C-Standard da einen
möglichen Unterschied vorsieht, oder kennst du Beispiele, wo der
Unterschied tatsächlich existiert?
Das einzige Beispiel, das mir einfällt und in die Richtung gehen könnte,
sind die Far-Pointer auf den 8086-Prozessoren, wo Segment und Offset-
Teil in einer 32-Bit-Variable zusammengefasst werden. Allerdings werden
diese Konstrukte, als xxxx:yyyy notiert, auch außerhalb von C gemeinhin
als "Adresse" bezeichnet. Damit wäre auch hier Pointer == Adresse.
Yalu X. schrieb:> Beispiele, wo Pointer keine physikalischen Adressen sind, sind ja alle> Rechner mit virtueller Speicherverwaltung.
Genau.
> Oben (und auch in früheren Posts) hast du aber das "physikalisch"> weggelassen:>>> Da wären wir wieder beim Thema, dass Zeiger keine Adressen sind.>> Wolltest du damit nur ausdrücken, dass der C-Standard da einen> möglichen Unterschied vorsieht, oder kennst du Beispiele, wo der> Unterschied tatsächlich existiert?> [...]
Naja, du bringst das Paradebeispiel ja gerade selbst.
Der Irrglaube, dass Zeiger == Adresse ist, geht ja dort schon in die
Hose, wenn das Segment überläuft. Natürlich entspricht jeder Zeiger
irgendwie einer verbogenen Adresse, aber das meinte ich nicht. Es gibt
einen Haufen Leute, die glauben, dass die Ganzzahl-Darstellung eines
Zeigers seine Adresse ist, und genau das ist eben nicht (immer) der
Fall. Das prangere ich regemäßig an und bekomme dafür regelmäßig auf die
Mütze.
Andere Beispiele wären die benannten Adressräume, mit denen man beim
avr-gcc den Umweg über 'progmem_...' und Konsorten sparen kann. In einer
Implementierung könnte da z.B. in den höherwertigen Bits eines
Speicherabbildes eines Zeigers kodiert werden, wie die Daten zu
beschaffen sind. Oder die Ausrichtung an den Wortgrenzen bei größeren
CISC-Prozessoren. Da könnte ein Compiler bei den komplexen Lade- und
Speicherinstruktionen noch multiplizieren.
Prinzipiell könnte ein Compiler auch einfach zufällige Werte als Zeiger
benutzen, die er dann in einer Tabelle wieder zuordnet. Dann würden sich
das Thema endlich von selbst erledigen. Der C99-Standard spricht
dahingehend nur eine Empfehlung ('is intended to be...') aus, nach der
die Ganzzahl-Darstellung eines Zeigers mit der Speicheranordnung der
Umgebung zusammenhängen soll. Aber die kann nun aussehen, wie sie
möchte.
Es läuft in jedem Fall darauf hinaus, dass die Ganzzahl-Darstellung
eines Zeigers keinen Rückschluss auf den physikalischen Speicherort
zulässt. Insbesondere lassen sich aus den Ganzzahl-Darstellungen zweier
Zeiger keine Aussagen über die Anordnung der dazugehörigen Objekte
(hintereinander, Abstand) treffen. Und schon garnicht sollte man
universell davon ausgehen, dass man einen Zeiger explizit auf irgendeine
physikalische Speicherstelle biegen kann, indem man ihn aus einer
Ganzzahl heruascastet.
In der Praxis sieht man aber haufeweise Tests von Pointern auf 0. Und
diese Programme laufen alle.
Es ist ja auch sehr bequem, einen Pointer auf 0 zu testen, z.B. um das
letzte Element einer verketteten Liste zu finden oder auf 0 zu setzen,
um ein Element wieder zu entfernen.
Peter
Johann L. schrieb:> Geht aber nur dann, wenn 0 keine gültige Adresse ist. Es gibt Systeme,> da ist 0 eine gültige Adresse. AVR ist eines davon: 0 ist gültige> EEPROM- und Flash-Adresse und bei den XMEGAs auch gültige RAM-Adresse> (I/O).
0 Ist auch keine gültige Adresse in C. Und wenn du damit den Nullpointer
meinst, ist schon ganz falsch. Ein Nullpointer zeigt auf NULL nicht auf
0.
Wie NULL definiert ist, kann unterschiedlich sein. Meistens liegt hat
er allerdings den Wert 0L. Aber wenn man ihn abfragt sollte man immer
NULL verwenden. Je nach Programmiersprache kann der Nullpointer auch den
Wert NUL,none,NIL NONE usw haben.
der mechatroniker schrieb:>> Warum, ist das ein neues Gesetz das von Dir eingeführt wurde?>> Weil ich von einem Fall rede, in dem es so ist.
Ah, so du redest von EINEM SPEZIELLEN FALL. Ach so ja klar, das hätten
wir natürlich alle auf Anhieb erraten können.
Egal welche Regeln man aufstellt, es "kann" immer man einen sinnvollen
Grund geben den zu brechen, weil eine ganz besondere Ausnahme gilt.
> Was ich natürlich vergessen hab zu erwähnen, ist, dass es in den Fällen,> von denen ich rede, oft eine adäquate Lösung wäre, ...
ja deinen speziellen Spezialfall hast du schon vergessen zu beschreiben
und die Worte:
>Ich muss nur an eines meiner "Lieblingsmuster" in fremdem Code denken>(viel zu oft gesehen):
sind nicht wirklich die richtigen Worte einen speziellen Spezialfall zu
beschreiben wo jeder gleich weiß das du einen ganz besonders einmaligen
Fall meinst.
Mikel M. schrieb:> Je nach Programmiersprache kann der Nullpointer auch den> Wert NUL,none,NIL NONE usw haben.
Null, None und Nil sind semantisch drei völlig verschiedene Dinge. So
einfach kann man die nicht in einen Topf werfen.
Es gibt Sprachen, in denen alle drei gemeinsam existieren.
Es ist eine andere Sache, wenn man in der zur Verfügung stehenden
Sprache alle drei Fälle über einen einzigen davon abwickeln muss.
Yalu X. schrieb:> Wolltest du damit nur ausdrücken, dass der C-Standard da einen> möglichen Unterschied vorsieht, oder kennst du Beispiele, wo der> Unterschied tatsächlich existiert?
Es gab mit den INMOS Transputern eine Prozessorfamilie, bei der die
Adressen vorzeichenbehaftet interpretiert wurden und daher 0 genau in
der Mitte des Adressraums lag. In der 16-Bit Version mit 64KB RAM also
genau mitten im RAM.
Genau genommen musste man also entweder darauf achten, dass diese
Adresse nicht vergeben wird, oder für den internen NULL-Zustand eines
Pointers einen anderen Wert als 0 verwenden. So dass
if (p==0)
in der Umsetzung des Codes also beispielsweise auf
if (p==0xFFFF)
rausliefe.
Roland H. schrieb:> Mikel M. schrieb:>> Je nach Programmiersprache kann der Nullpointer auch den>> Wert NUL,none,NIL NONE usw haben.>> Null, None und Nil sind semantisch drei völlig verschiedene Dinge. So> einfach kann man die nicht in einen Topf werfen.
Ich habe nie behauptet das sie nur dafür stehen. Aber sie stehen je
nach Umgebung für einen Nullpointer, und darum ging es. Deine Aussage
müßte dagegen heißen " können auch für semantisch Unterschiedliche Dinge
stehen" so wie sie da steht ist Deine Aussage falsch.
A. K. schrieb:>> Genau genommen musste man also entweder darauf achten, dass diese> Adresse nicht vergeben wird,
Um sich einen Wert zu generieren braucht man ja nur ein malloc machen
und den dafür zu verwenden. Das hat aber den Nachteil das es schwer ist
einen Nullpointer zugriff festzustellen.
Die Adresse 0L liegt entweder im geschützten Bereich, oder besteht
sogar aus ROM, Es ist der RESETvektor bei vielen cpus. Sprich es wird
sofort eine Bereichsverletzung festgestellt und das Programm beendet
bevor schlimmeres passiert. Wenn man sich selber einen mit malloc holt,
liegt es im erlaubten Bereich, den man dann regelmäßig testen sollte ob
er überschrieben wurde. Man kann dann also nur sagen, irgendwann ist ein
Nullpointerschreibzugriff gewesen.
> Pointers einen anderen Wert als 0 verwenden. So dass> if (p==0)> in der Umsetzung des Codes also beispielsweise auf> if (p==0xFFFF)> rausliefe.
Nein der Code sollte immer heißen
if ( p == NULL ) ...
, nur damit stellt man sicher das der Code portable bleibt. Wie NULL
dann definiert ist ist eine andere Sache. Ganz falsch wäre es zu
schreiben
if (p) ...
Mikel M. schrieb:> if ( p == NULL ) ...> , nur damit stellt man sicher das der Code portable bleibt.
C99: "An integer constant expression with the value 0, or such an
expression cast to type void *, is called a null pointer constant. If a
null pointer constant is converted to a pointer type, the resulting
pointer, called a null pointer, is guaranteed to compare unequal to a
pointer to any object or function."
und zu ==
"One of the following shall hold: [...]
—one operand is a pointer and the other is a null pointer constant."
Folglich ist
if (p == 0)
ein völlig normales Statement und muss die gleiche Wirkung haben wie
if (p == (void *)0)
oder
if (p == NULL)
und ebenso ist
p = 0;
das Gleiche wie
p = NULL;
unabhängig davon, ob dabei intern vielleicht
move 0x7FFF to p
rauskommt.
> Ganz falsch wäre es zu schreiben
Keineswegs, aus dem gleichen Grund.
Mikel M. schrieb:> Ganz falsch wäre es zu schreiben if (p) ...A. K. schrieb:> Keineswegs, aus dem gleichen Grund.> C99: "An integer constant expression with the value 0, or such an> expression cast to type void *, is called a null pointer constant. If a> null pointer constant is converted to a pointer type, the resulting> pointer, called a null pointer, is guaranteed to compare unequal to a> pointer to any object or function."
Zustimmung. Streng genommen deckt dein Zitat die Fragestellung aber
nicht vollständig ab, und man muss noch ein weitere Stellen des
Standards zu Rate ziehen. Ich habe sie spaßeshalber mal
zusammengetragen:
Welche Datentypen sind überhaupt als Bedingung in einer If-Anweisung
erlaubt?
"The controlling expression of an if statement shall have scalar
type."
Aha, die Bedingung muss also mindestens ein skalarer Typ sein. Ist auch
ein Pointer ein skalarer Type?
"Arithmetic types and pointer types are collectively called scalar
types."
Ja, es spricht also prinzipiell nichts dagegen, einen Pointer als
Bedingung in einer If-Anweisung zu verwenden. Aber wird der Pointer als
Bedingung auch erwartungsgemäß ausgewertet?
"In both forms [also sowohl in der If- als auch der If-Else-
Anweisung], the first substatement is executed if the expression
[damit ist die Bedingung gemeint] compares unequal to 0."
Der Pointer p wird also auf Ungleichheit zu 0 überprüft.
Dumm nur, dass p ein Pointer und 0 ein Integer ist. Wie in C
Ungleichheit zwischen unterschiedlichen Datentypen gehandhabt wird,
schauen wir bei den Equality-Operators (== und !=) nach:
"If both of the operands have arithmetic type, […]"
Was zur Hölle sind "arithmetic types"?
"Integer and floating types are collectively called arithmetic types."
Ok, Pointer gehören wohl nicht dazu, also ist die Regel im vorletzten
Zitat hier nicht anwendbar und wir gehen zu nächsten über:
"If one operand is a pointer and the other is a null pointer constant,
[…]"
Einer der Operanden ist ein Pointer, keine Frage. Aber ist der andere
eine "null pointer constant"?
"An integer constant expression with the value 0, or such an
expression cast to type void *, is called a null pointer constant."
Yep. Eine Integer-Null ist tatsächlich eine "null pointer constant", und
wir können die Regel anwenden:
"[…] the null pointer constant is converted to the type of the
pointer."
Die 0 wird also in einen Null-Pointer konvertiert, der den gleichen Typ
wie p hat.
Somit wird in der If-Bedingung "if(p)" der Pointer p mit einem Null-
Pointer des gleichen Typs verglichen.
"If a null pointer constant is converted to a pointer type, the
resulting pointer, called a null pointer , is guaranteed to compare
unequal to a pointer to any object or function."
Zeigt p also auf auf ein Objekt oder eine Funktion, dann liefert der
Vergleich "ungleich", d.h. die If-Abfrage ist erfolgreich, und die
Anweisung nach der If-Bedingungen wird ausgeführt.
Was passiert aber, wenn p ein Null-Pointer ist? Dann wird ein Null-
Pointer mit einer zum Null-Pointer konvertierten Null-Pointer-Konstante
verglichen.
"Two pointers compare equal if and only if both are null pointers,
[…], […], or […]"
Der erste Teil des Satzes reicht schon: Da beide Operanden Null-Pointer
sind liefert der Vergleich "gleich", d.h die If-Abfrage ist nicht
erfolgreich, und die Anweisung nach der If-Bedingungen wird nicht
ausgeführt.
Zusammengefasst wird also in
1
if(p)
2
<Anweisung>
<Anweisung> genau dann ausgeführt, wenn p kein Null-Pointer ist.
Damit verhält sich diese If-Anweisung exakt gleich wie
1
if(p!=NULL)
2
<Anweisung>
Übrigens ist aus ähnlichen Gründen auch der Ausdruck
1
!p
legal und liefert 1, wenn p ein Null-Pointer ist, und sonst 0. Hier sind
die entsprechenden Ausschnitt des Standards:
"The operand of the unary + or - operator shall have arithmetic type;
of the ~ operator, integer type; of the ! operator, scalar type."
"The result of the logical negation operator ! is 0 if the value of
its operand compares unequal to 0, 1 if the value of its operand
compares equal to 0. The result has type int. The expression !E is
equivalent to (0==E)."
Damit sind Konstrukte wie
1
if(!(fp=fopen(filename,"r"))){
2
// Fehlermeldung, Abbruch
3
}
4
// Datei lesen
5
fclose(fp);
(wie ich sie auch selber gerne verwende :)) perfekt in Ordnung.
Du beziehst dich auf C99. Ich mußte mal mit einem Compiler arbeiten bei
dem NULL != 0 definiert war, da ist man mit so einer Programmierung dann
auf die Schnauze gefallen. Da ich immer versuche maximal portablen Code
zu schreiben, kommt für mich eine solche Programmierung nicht in Frage.
Zum anderen empfinde ich es als sauberen, lesbaren Programmierstiel wenn
man Pointer gezielt gegen NULL vergleicht. .. aber Geschmäcker sind ja
verschieden.
Mikel M. schrieb:> Du beziehst dich auf C99. Ich mußte mal mit einem Compiler arbeiten bei> dem NULL != 0 definiert war, [...]
Ich programmiere seit 1984 in C und so ein Compiler ist mir in den 28
Jahren noch nicht untergekommen. Sag doch mal, was das denn für ein
"Compiler" war.
Mikel M. schrieb:> Du beziehst dich auf C99.
Hauptsächlich deshalb, weil man in dem C99 (oder C11) PDF leichter
suchen und daraus kopieren kann, als bei der Wordstar-ähnlichen
Textwüste von C89 oder dem Papier früherer Standards.
Nachgesehen habe ich dort grad nicht, aber gehe davon aus, dass dies
nicht erst mit C99 so definiert wurde. Die if(p) oder if(!p) Varianten
werden nämlich seit Anbeginn von C recht häufig verwendet.
> Ich mußte mal mit einem Compiler arbeiten bei> dem NULL != 0 definiert war,
Ok. Das ist natürlich hart. Wenn man nicht nur Compiler berücksichtigen
muss, die sich an den Standard halten, sondern auch jene, die es nicht
tun.
Frank M. schrieb:> Ich programmiere seit 1984 in C und so ein Compiler ist mir in den 28> Jahren noch nicht untergekommen.
Mir auch noch nicht, allerdings stand ich bei der Implementierung für
besagte Transputer vor exakt dieser Alternative. Denn die Variante, eine
programmierte 0 intern in 0x7FFF oder so umzusetzen, war mir zu kritisch
und zu aufwendig. Es kann sein, dass eine andere Implementierung den Weg
gegangen ist, NULL anders zu belegen als üblich.
Da das Problem de fakto nur die eher exotische 16-Bit Plattform betraf
und ich genug real existierende Programme kannte, die andernfalls auf
die Nase fliegen, entschied ich mich dafür, NULL eine 0 sein zu lassen.
Wer das 16-Bit Modell verwendete (das ohnehin nur im embedded-Sektor
Sinn ergab), der musste diese nicht so ganz zum Standard konforme
Eigenheit eben auf der Rechnung haben. Ein Blick ins Mapfile und du
weisst, ob das zum Problem werden kann oder nicht.
A. K. schrieb:> Alle Achtung! War dir grad langweilig? ;-)
Das nicht. Aber da ich plötzlich selber etwas unsicher mit diesen
if(p)-Konstrukten wurde, diese aber oft verwende und ich natürlich, wenn
immer möglich, nur standardkonformen Code schreiben möchte :), habe ich
dann doch mal etwas genauer nachgesehen.
Mikel M. schrieb:> Du beziehst dich auf C99.
Ja. Ich habe jetzt aber keine Lust, auch noch den C90-Standard zu
durchwühlen ;-)
> Ich mußte mal mit einem Compiler arbeiten bei dem NULL != 0 definiert> war, da ist man mit so einer Programmierung dann auf die Schnauze> gefallen.
Das könnte aber genauso gut auch ein Implementierungsfehler des
Compilers bzw. der Standardbibliothek gewesen sein. Aber ärgerlich ist
so etwas natürlich schon und macht in Zukunft vorsichtig.
> Zum anderen empfinde ich es als sauberen, lesbaren Programmierstiel wenn> man Pointer gezielt gegen NULL vergleicht. .. aber Geschmäcker sind ja> verschieden.
Eigentlich bin ich voll deiner Meinung. So versuche ich bspw. generell,
implizite Typumwandlungen zu vermeiden und verwende stattdessen explizi-
te Casts. Nur die Verwendung von Pointern als Bedingung hat sich (nicht
nur bei mir) so sehr eingebrannt, dass ich den expliziten Vergleich mit
NULL fast schon als hässlich empfinde ;-)
Entsprechend lasse ich auch meist den expliziten Vergleich eines Chars
mit '\0' weg.
Ähnliche Beispiele:
1
// Iteration ueber verkettete Liste
2
for(lptr=list;lptr;lptr=lptr->next)...
3
4
// Iteration über String
5
for(cptr=str;*cptr;cptr++)...
6
7
// Enthält der String einen Klammeraffen (die Position ist egal)?
Frank M. schrieb:> Sag doch mal, was das denn für ein "Compiler" war.
Ist schon lange, her, war ne Industrieanwendung, vor der www zeit, ich
glaube nicht das ich da noch Unterlagen zu habe. Ob es den heute noch
gibt, k.a. bin seit dem schön öfters umgezogen. Zu dem Zeitpunkt war
vieles noch nicht so eingebürgert und viele kochten ihr eigens
Süppchen...
> Hauptsächlich deshalb, weil man in dem C99 (oder C11) PDF leichter ...
ist durch aus nachvollziehbar. Ich gehe auch davon aus das es heute zu
99,99 % funktioniert. Aber wenn man einmal drauf sensibilisiert wurde
...
Yalu X. schrieb:> Eigentlich bin ich voll deiner Meinung. So versuche ich bspw. generell,> implizite Typumwandlungen zu vermeiden und verwende stattdessen explizi-> te Casts.
Suum quique. Ich halte es nämlich eher andersrum. Da Casts so ziemlich
alles fressen, was man ihnen vorsetzt, ziehe ich es vor, dem Compiler
eine Chance für Warnungen zu geben. Denn mit Cast kriegst du ziemlich
sicher keine, egal wie blödsinnig es ist was da steht.
> Entsprechend lasse ich auch meist den expliziten Vergleich eines Chars> mit '\0' weg.
Mir ist vor langer Zeit mal Quellcode untergekommen, in dem jemand
überaus konsequent immer dort NULL verwendet hatte, wo '\0' korrekt
gewesen wäre. Was deutlich macht, dass sein Compiler NULL als 0
definierte. Der von mir verwendete aber als (void *)0. War etwas Arbeit.
Mikel M. schrieb:> Aber sie stehen je> nach Umgebung für einen Nullpointer, und darum ging es.
Das tun sie nicht immer, da ein "Nullpointer" eine echte Instanz
ausschließt.
> Deine Aussage> müßte dagegen heißen " können auch für semantisch Unterschiedliche Dinge> stehen" so wie sie da steht ist Deine Aussage falsch.
Nein, sie stehen semantisch immer für unterschiedliche Dinge, weil sie
jeweils etwas anderes bedeuten. Die Aussage war richtig.
Es ist eine andere Sache, wenn eine Umgebung zwei oder drei der
Bedeutungen in einem Konstrukt abbildet.
Seht ihr, und genau das meinte ich damit, dass Zeiger keine Adressen
sind... quod erat demonstrandum. Jetzt hat es sich quasi von selbst
erledigt :-)
Der Null-Zeiger war aber auch in ANSI-C irgendwo um 1989 schon mit '0'
definiert.
Korrekt. Davor auch.
ANSI-C aka C89: "An integral constant expression with the value 0, or
such an expression cast to type void *, is called a null pointer
constant. If a null pointer constant is assigned to or compared for
equality to a pointer, the constant is converted to a pointer of that
type. Such a pointer, called a null pointer, is guaranteed to compare
unequal to a pointer to any object or function."
K&R formulierten das 1978 wie üblich weniger formell: "C guarantees that
no pointer that validly points at data will contain zero, so return
value of zero [char *alloc() - A.K.] can be used to signal an abnormal
event, in this case, no space. We write NULL instead of zero, however,
to indicate more clearly that this is a special value for a pointer."
Hier wird auch der historische Kontext dieser Abnormität deutlich: "The
compilers currently allow a pointer to be assigned to an integer, an
integer to a pointer, and a pointer to a pointer of another type. The
assignment is a pure copy operation, with no conversion. This usage is
nonportable, and may produce pointers which cause addressing exceptions
when used. However, it is guaranted that assignment of the constant 0 to
a pointer will produce a null pointer distinguishable from a pointer to
any object".
Die Verwendung von 0 als "null pointer value" aka NULL hat sich also
eher faktisch als formell entwickelt und die Besonderheit dieses Wertes
wurde dann a posteriori festgelegt. Durch diese damalige faktische
Typfreiheit zwischen Zeigern und "int" (nicht in K&C, aber in den
damaligen Compilern) ist auch unvermeidlich, dass
if (p)
von Anfang an mit
if (p != NULL)
identisch sein musste. Da formale Präzision wahrlich keine Stärke von
K&R war, darf man das getrost als Festlegung des damaligen C betrachten,
auch wenn es nicht so klingt.
Sven P. schrieb:> Seht ihr, und genau das meinte ich damit, dass Zeiger keine Adressen> sind...
Heute nicht mehr. Vor K&R-C, also in Ritchie-C, da waren sie es noch:
"7.14.1 lvalue = expression
[...]
When both operands are int or pointers of any kind, no conversion ever
takes place; the value of the expression is simply stored into the
object referred to by the lvalue. Thus it is possible to generate
pointers which will cause addressing exceptions when used."
http://cm.bell-labs.com/who/dmr/cman.pdf
A. K. schrieb:> Yalu X. schrieb:>> Eigentlich bin ich voll deiner Meinung. So versuche ich bspw. generell,>> implizite Typumwandlungen zu vermeiden und verwende stattdessen explizi->> te Casts.>> Suum quique. Ich halte es nämlich eher andersrum. Da Casts so ziemlich> alles fressen, was man ihnen vorsetzt, ziehe ich es vor, dem Compiler> eine Chance für Warnungen zu geben. Denn mit Cast kriegst du ziemlich> sicher keine, egal wie blödsinnig es ist was da steht.
Ich muss das oben Geschriebene etwas präzisieren:
Ich verwende die Casts nur dort, wo durch die Typumwandlung der Werte-
bereich eingeschränkt wird, also bspw. bei der Umwandlung von int nach
char (aber nicht im umgekehrten Fall). Die Casts schreibe ich auch nicht
gedankenlos hin, sondern erst dann, wenn ich mich vergewissert habe,
dass durch die Wertebereichseinschränkung kein Fehler entstehen kann.
Das sind nämlich genau die Fälle, wo der Compiler (bei entsprechendem
Warning-Level) meckern würde, wenn ich den Cast wegließe.
Wenn du erst mal den Compiler meckern lässt, dann nachdenkst, und nach
der Beseitigung aller Bedenken die Casts einfügst, läuft das natürlich
auf das Gleiche hinaus.
Böse wird es nur, wenn man die Compiler-Warnungen nicht aktiviert (bzw.
nicht beachtet) oder die Casts ohne Überlegung einsetzt.