Forum: Compiler & IDEs if-Abfragen verpönt?


von blubb (Gast)


Lesenswert?

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.
1
...Schreiboperation...
2
if (buffer_write_index >= buffer_size) buffer_write_index = 0;

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?

von Tom (Gast)


Lesenswert?

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.

von B. S. (bestucki)


Lesenswert?

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.

von Coder (Gast)


Lesenswert?

Ich würde
1
if (buffer_write_index >= buffer_size) buffer_write_index = 0;
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

von (prx) A. K. (prx)


Lesenswert?

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.

von Reinhold_B (Gast)


Lesenswert?

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

von Thorsten (Gast)


Lesenswert?

Und vorallem ist:

if (buffer_write_index >= buffer_size) buffer_write_index = 0;

semantisch nicht äquivalent zu:

buffer_write_index %= buffer_size;

Thorsten

von (prx) A. K. (prx)


Lesenswert?

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.

von (prx) A. K. (prx)


Lesenswert?

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.

von Sam P. (Gast)


Lesenswert?

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:
1
if (buffer_write_index == buffer_size) buffer_write_index = 0;

oder
1
if (buffer_write_index >= buffer_size) buffer_write_index = 0;

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 ;)

von (prx) A. K. (prx)


Lesenswert?

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.

von Sam P. (Gast)


Lesenswert?

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:
1
#define BUFFER_SIZE_EXP 5
2
#define BUFFER_SIZE (1<<BUFSIZE_EXP)
3
#define BUFFER_INDEX_MASK (BUFFER_SIZE-1)

von blubb (Gast)


Lesenswert?

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?

von Sam P. (Gast)


Lesenswert?

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.

von (prx) A. K. (prx)


Lesenswert?

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.

von (prx) A. K. (prx)


Lesenswert?

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.

von Rufus Τ. F. (rufus) Benutzerseite


Lesenswert?

A. K. schrieb:
> Wenn man davon absieht, dass Modulo-Operationen schweineteuer sein
> können

Können, aber nicht müssen. Wenn man beispielsweise Puffergrößen als 
Zweierpotenzen definiert, dann nicht.

von R. M. (rmax)


Lesenswert?

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:
1
while (buffer_write_index >= buffer_size)
2
    buffer_write_index -= buffer_size;

von Sam P. (Gast)


Lesenswert?

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:
1
while (buffer_write_index >= buffer_size) buffer_write_index -= buffer_size;

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.

von Klaus (Gast)


Lesenswert?

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 %

von Yalu X. (yalu) (Moderator)


Lesenswert?

> Sind if-Abfragen so verpönt?

Nein, überhaupt nicht, zumindest nicht in C.

1
if (buffer_write_index >= buffer_size) buffer_write_index = 0;

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:
1
buffer_write_index++;
2
if (buffer_write_index >= buffer_size) buffer_write_index = 0;

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:
1
buffer_write_index = (buffer_write_index + 1 ) % buffer_size;

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.

von Sam P. (Gast)


Lesenswert?

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".

von (prx) A. K. (prx)


Lesenswert?

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.

von Sam P. (Gast)


Lesenswert?

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.

von Coder (Gast)


Lesenswert?

Interessant...
1
buffer_write_index %= buffer_size;

ist zwar kürzer, da müsste ich aber mehr überlegen, als
1
if (buffer_write_index >= buffer_size) buffer_write_index = 0;

von Sam P. (Gast)


Lesenswert?

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
int wrap_index(int index, int size) {
2
  if ((size-1)&size == 0) {
3
    return index & (size-1)
4
  } else {
5
    while (index >= size) index -= size;
6
    return index;
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.

von Roland H. (batchman)


Lesenswert?

> 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!

von Frank M. (ukw) (Moderator) Benutzerseite


Lesenswert?

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 ;-)

von Peter D. (peda)


Lesenswert?

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

von Mikel M. (mikelm)


Lesenswert?

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.

von Yalu X. (yalu) (Moderator)


Lesenswert?

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.

von Läubi .. (laeubi) Benutzerseite


Lesenswert?

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/...

von Oliver S. (oliverso)


Lesenswert?

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

von Peter D. (peda)


Lesenswert?

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

von Frank M. (ukw) (Moderator) Benutzerseite


Lesenswert?

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 :-)

von der mechatroniker (Gast)


Lesenswert?

> 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

von Roland H. (batchman)


Lesenswert?

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:
1
typedef unsigned short buffer_size_t;
2
3
class Buffer {
4
  inline buffer_size_t const getNextPos(buffer_size_t const pos) const {
5
    return pos == (size - 1) ? 0 : pos + 1;
6
  }
7
};
8
9
void Buffer::write(char const c) {
10
  Contract::require(writePos < size, Error_IndexOutOfBounds);
11
12
  buffer[writePos] = c;
13
  writePos = getNextPos(writePos);
14
15
  Contract::ensuring(writePos < size, Error_IndexOutOfBounds);
16
}
17
18
class Contract {
19
  static void require(bool const expression, Error const error) {
20
    if (expression == false) {
21
      __asm volatile ( "bkpt 0" );
22
    }
23
  }
24
};

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.

von Oliver S. (oliverso)


Lesenswert?

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

von Andreas H. (ahz)


Lesenswert?

Sam P. schrieb:
> "can't happen" passiert

Der Spruch ist so genial (& wahr), den sollte man sich als Signatur 
zulegen.

Grüße
Andreas

von Roland H. (batchman)


Lesenswert?

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.

von Peter D. (peda)


Lesenswert?

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

von Edson (Gast)


Lesenswert?

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?

von xfr (Gast)


Lesenswert?

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.

von der mechatroniker (Gast)


Lesenswert?

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.

von Vlad T. (vlad_tepesch)


Lesenswert?

manchmal gehört es aber auch zum Interface, dass der Pointer NULL sein 
kann.

von Mikel M. (mikelm)


Lesenswert?

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.

von Peter D. (peda)


Lesenswert?

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

von Johann L. (gjlayde) Benutzerseite


Lesenswert?

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).

von Sven P. (Gast)


Lesenswert?

Da wären wir wieder beim Thema, dass Zeiger keine Adressen sind. Ja ich 
weiß, man steinigt mich jedes Mal dafür, aber ich kanns ja auch nicht 
ändern.

von Roland H. (batchman)


Lesenswert?

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.
###

von Detlev T. (detlevt)


Lesenswert?

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_t this = malloc(sizeof(item_t));
4
  
5
  if(this)
6
   {
7
     /* Daten initialisieren */
8
   }
9
  return this;
10
}

Wenn im Speicher kein Platz mehr ist, wird NULL zurückgegeben und die 
aufrufende Routine muss das halt abfangen.

von der mechatroniker (Gast)


Lesenswert?

> 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.

von Peter D. (peda)


Lesenswert?

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

von Sven P. (Gast)


Lesenswert?

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.

von Yalu X. (yalu) (Moderator)


Lesenswert?

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.

von Sven P. (Gast)


Lesenswert?

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...

von Yalu X. (yalu) (Moderator)


Lesenswert?

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.

von Sven P. (Gast)


Lesenswert?

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.

von Peter D. (peda)


Lesenswert?

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

von Mikel M. (mikelm)


Lesenswert?

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.

von Mikel M. (mikelm)


Lesenswert?

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.

von Roland H. (batchman)


Lesenswert?

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.

von (prx) A. K. (prx)


Lesenswert?

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.

von Mikel M. (mikelm)


Lesenswert?

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.

von Mikel M. (mikelm)


Lesenswert?

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) ...

von (prx) A. K. (prx)


Lesenswert?

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.

von Yalu X. (yalu) (Moderator)


Lesenswert?

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.

von (prx) A. K. (prx)


Lesenswert?

Alle Achtung! War dir grad langweilig? ;-)

von Mikel M. (mikelm)


Lesenswert?

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.

von Frank M. (ukw) (Moderator) Benutzerseite


Lesenswert?

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.

von (prx) A. K. (prx)


Lesenswert?

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.

von (prx) A. K. (prx)


Lesenswert?

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.

von Yalu X. (yalu) (Moderator)


Lesenswert?

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)?
8
if (strchr(str, '@')) ...

von Mikel M. (mikelm)


Lesenswert?

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 
...

von (prx) A. K. (prx)


Lesenswert?

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.

von Roland H. (batchman)


Lesenswert?

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.

von Sven P. (Gast)


Lesenswert?

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.

von (prx) A. K. (prx)


Lesenswert?

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.

von (prx) A. K. (prx)


Lesenswert?

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

von Yalu X. (yalu) (Moderator)


Lesenswert?

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.

Bitte melde dich an um einen Beitrag zu schreiben. Anmeldung ist kostenlos und dauert nur eine Minute.
Bestehender Account
Schon ein Account bei Google/GoogleMail? Keine Anmeldung erforderlich!
Mit Google-Account einloggen
Noch kein Account? Hier anmelden.