C++ - Selbe Rechnung mit selben Operanden liefert unterschiedliche Ergebnisse?!

Ich habe mich auch gewundert, wieso die pow()-Funktion überhaupt mit log()'s rechnet. Ist das für die Berechnung von Potenzen von double-Werten notwendig?
Die Definition von Potenzen reeller Zahlen wird mathematisch über die Exponentialfunktion und den Logaritmus zur Basis e definiert. Zahlen in der Informationsverarbeitung sind naturgemäß allerdings eine Teilmenge der rationalen Zahlen, sodass man das auch anders definieren könnte. Außerdem ist den Informatikern die Zahl 2 viel lieber als e. Die Funktionen exp und log2 werden intern vermutlich mithilfe einer Interpolation mithilfe einiger festen Werte berechnet, was wesentlich schneller gehen sollte, als eine Schleife. Für rationale Zahlen (also double) bräuchte man auch mindestens zwei solcher Schleifen und müsste noch eine Division durchführen. Ich sehe auch das Problem, dass Divident und Divisor sehr groß werden könnten, obwohl das Ergebnis klein bleibt. Falls man alternativ häufiger kleine Divisionen durchführt, können sich Rundungsfehler aufschaukeln.
 
Ich frag jetzt einfach mal ganz ketzerisch: Was spricht gegen folgendes?

[src=cpp]
#include <string>
#include <sstream>

std::string decToHex(long long number)
{
std::stringstream s;
s << std::hex << number;
return s.str();
}
[/src]

Wozu haben wir denn die standard library …

Zur Exception bei negativen Zahlen: Gefällt mir in dem Fall nicht. Exceptions sind dafür da, Fehlerzustände zu melden, die während eines korrekten Aufrufs der Funktion auftreten und von der Funktion selbst nicht sinnvoll gehandhabt werden können. Die nicht negativ-fähige Funktion mit einer negativen Zahl zu füttern, ist kein korrekter Aufruf. Das ist ein Bug. Anders gesagt: Der Contract der Funktion mit dem Caller sagt: »nix Negatives«. Trotzdem etwas Negatives reinzuschieben, ist eine »contract violation«, die normalerweise mit einer Assertion geprüft wird. In diesem Fall am Funktionsanfang mit:

[src=cpp]assert(lngDecInput >= 0);[/src]

Im Debug-Modus terminiert das Programm mit dem entsprechenden Assertion-Fehler. Im Release passiert … irgendwas. Ist aber nicht so wild. Der falsche Aufruf ist ein Bug. Der muss raus, damit er im Produktivbetrieb niemals auftreten kann.

Die Idee dahinter ist die, dass i.d.R. der Caller die besseren Kontextinformationen hat, um a) zu wissen, ob an der Stelle überhaupt ungültige Daten ankommen können und b) mit ungültigen Inputdaten umzugehen. Der Caller kann dann auch entscheiden, wieviel Performance verbraten wird, um gültige Daten sicherzustellen. Die Funktion selbst verlässt sich auf gültigen Input und drückt damit auch nicht ggf. unnötig die Performance.
 
  • Thread Starter Thread Starter
  • #23
// Du möchtest genau ein mal pro Exponent durchlaufen. Daher nur < statt <=
for (unsigned int i = 0; i < exponent; i++)

Sollte denn nicht eigentlich int i = 0; i < exponent das gleiche Ergebnis liefern wie int i = 1; i <= exponent?
Ansonsten hatte ich das wohl schon selbst ungefähr dahin korrigiert, wie du es jetzt aufgeschrieben hast. (Obwohl natürlich die größeren Datentypen vermutlich Sinn machen.)
Aaaach, aber mit der Variante i = 0 ist natürlich auch der Fall i^0 abgedeckt, also doch schlauer als meine Variante :D

Wenn das allerdings tatsächlich wesentlich rechenaufwändiger ist bei großen Zahlen, dann belasse ich es lieber bei pow(). Scheint ja so jetzt auch zu gehen.

--- [2018-09-10 22:13 CEST] Automatisch zusammengeführter Beitrag ---

Ich frag jetzt einfach mal ganz ketzerisch: Was spricht gegen folgendes?
[...]
Außer, dass es dann für mich kaum einen Lerneffekt hätte, vermutlich nichts. Sollte ich aber tatsächlich mal eine solche Konversion irgendwo implementieren wollen, weiß ich jetzt schonmal, dass ich auch einfach bereits vorhandene und damit vermutlich weniger fehleranfällige Funktionen nutzen kann :D Danke dafür :T

Zu der Sache mit den exceptions: Ich habe das jetzt erstmal so gelöst:

[src=cpp]const short ERR_NEUTRAL = 0;
const short ERR_SUCCESS = 1;
const short ERR_FORMAT = -1;
const short ERR_NEGATIVE_INPUT = -2;

short err = ERR_NEUTRAL;[/src]

Zu Beginn jeder Memberfunktion wird err auf ERR_NEUTRAL gesetzt. Die Namensgebung ist vielleicht ungünstig. Gemeint ist, wenn err am Ende immer noch NEUTRAL ist, hat iwas nicht funktioniert, es weiß aber niemand was. Ansonsten wird eben bei negativem Input, sonstigen Formatfehlern (z.B. eine '2' in der binären Eingabe) oder halt erfolgreicher Umwandlung der Wert entsprechend gesetzt.
 
Zuletzt bearbeitet:
Daran habe ich auch kurz gedacht, einfach die Standardlibrary zu bemühen. Da Kenobi aber offensichtlich etwas lernen möchte und sich selbst etwas ausprobieren, wollte ich dahingehend Hilfestellungen geben.

Bezüglich deinem Assertion vs Exception:
Ich verstehe, wenn du sagst, dass man durch Assertions den Entwickler darauf aufmerksam machen will, dass da was komisches passiert ist und der das beheben soll. Allerdings finde ich es viel schlimmer, wenn eine Funktion so implementiert ist, dass man fehlerhaften Input reinwerfen kann und die "nur darauf vertraut", dass das schon alles passen wird.

Zudem möchte ich noch darauf hinweisen, dass es durchaus eine Funktion sein kann, die Nutzerinput weiterverarbeiten soll. Es gilt zwar die Regel, dass man niemals auf den Nutzerinput vertrauen darf, doch wenn man eine Funktion eine Exception werfen lässt, so kann man den Fehlerfall abfangen, sollte der Nutzerinput nicht ausreichend geprüft worden sein. Lässt man eine Assertion auslösen, so kann man das nicht mehr abfangen (zumindest soweit ich weiß).
Ich bevorzuge daher die Exception-Variante vor der Assertion-Variante.
 
Kenobi van Gin schrieb:
Außer, dass es dann für mich kaum einen Lerneffekt hätte, vermutlich nichts.
Hätte zumindest den Lerneffekt, dass man vieles gar nicht selber machen muss. ;) Aber passt. Aus Interesse was selber implementieren ist vollkommen ok.

Ich hab jetzt in deine zweite Version auch mal genauer reingeschaut.


Zu den Problemen der ungarischen Notation lass mich eins hinzufügen: Was wäre denn dein Namenspräfix für diesen Typ?
[src=cpp]std::unordered_map<std::uint64_t, std::vector<MyClass>>::const_iterator[/src]
Denk daran: Wenn du in einem Jahr deinen Code wieder anschaust, muss dir das Kürzel auf Anhieb eindeutig den Typ verraten, sonst hast du nichts gewonnen.


Einige Variblen deklarierst du am Funktionsanfang. Das kenne ich eher aus uraltem C-Code. Nachteil ist, dass notfalls uninitialisierte Variablen in einem viel größeren Scope verfügbar sind als nötig. Deswegen versuche, die Variablen idealerweise zusammen mit der ersten Verwendung zu deklarieren.

  • intMaxPower – kann direkt vor die erste for-Schleife.
  • lngRemaining – kann direkt vor den Block mit den geschachtelten for-Schleifen.
  • sConv – ist unbenutzt, schau mal auf deine Compilerwarnungen.
  • strResult – OK, kann da bleiben, weil in der Variablen der Returnwert zusammengebaut wird. Weil ich ein pingeliger Hund bin, würde ich sie intuitiv trotzdem vor den geschachtelten for-Block schieben.
  • strTmp – kann sterben. Du kannst im switch direkt an strResult anhängen.

Die Potenzierung kann immer noch schief gehen. Dieser Ausdruck:
[src=cpp](long long) pow(16.0, intPower)[/src]
schneidet den Nachkommateil des Ergebnisses ab. Durch die Fließkomma-Ungenauigkeiten kann es dir aber durchaus passieren, dass 5² als Ergebnis 24,9999965456 liefert. Abschneiden ist dann nur so mittel ideal. ;) Du willst auf den nähesten Integer runden. Am besten bau dir eine kleine Funktion. Sowas wie:
[src=cpp]#include <cmath>
#include <cstdint>

long long integral_pow(long long base, int exp)
{
return std::llround(std::pow(static_cast<long double>(base), exp));
}[/src]

Btw: Bitte nie den klassischen C-Cast verwenden – also sowas wie (long long)foo –, denn der führt nicht zu einem Compilerfehler, auch wenn der Cast Unfug ist. Die C++-Casts sind immer besser: meistens static_cast<>, bei polymorphen Objekten auch mal dynamic_cast<>. Selten braucht man für Byte/Bit-Schubsereien auch mal reinterpret_cast<>. Aber Vorsicht damit: Das ist der, der die Compilerchecks abschaltet. Und Finger weg vom const_cast<>!


Zum Algorithmus selbst hat Roin schon in #4 eine feine Vereinfachung gepostet. Ich stell noch eine Implementierung daneben, die die Sache aus einer ganz anderen Richtung angeht.

Integer im Computer haben ein paar nette Eigenschaften. Besonders nützlich: Jeweils 4 Bit lassen sich mit einer einzelnen Hex-Ziffer repräsentieren. Also könntest du in 4-Bit-Schritten durch deinen Integer marschieren und jeden dieser Bit-Blöcke mit deinem switch oder mit Roins Lookup-Table in eine Hex-Ziffer umwandeln. Hex-Ziffern hintereinander hängen und fertig.

So könnte das aussehen:
[src=cpp]#include <array>
#include <cstddef>
#include <cstdint>
#include <string>

namespace {
constexpr static const std::array<char, 16> hex_digits_lut {
'0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'A', 'B', 'C', 'D', 'E', 'F'
};
}

std::string to_hex(std::uint64_t number)
{
// 0 must be special-cased. Otherwise our “no leading zeros” approach
// below would return an empty string.
if (number == 0) {
return "0";
}

constexpr const int number_width = sizeof(number) * 8; // bits
constexpr const int digit_width = 4; // bits
constexpr const std::uint64_t mask = 0xf;
std::string hex;

for (int shift_by = number_width - digit_width; shift_by >= 0; shift_by -= digit_width) {
// Shift the original number by so many bits that the currently processed
// 4-bit block is in the right-most position.
const auto shifted_number = number >> shift_by;

// Zero out everything but the current 4-bit block.
// This gives us the index into `hex_digits_lut`.
const auto digit = shifted_number & mask;

// The `if` avoids leading zeros in the returned string.
if (digit != 0 || not hex.empty()) {
hex += hex_digits_lut[digit];
}
}

return hex;
}


#include <cassert>
#include <iostream>

void check(std::uint64_t number, std::string hex)
{
const auto result = to_hex(number);
std::cout << result << '\n';
assert(result == hex);
}

int main()
{
check(0, "0");
check(1, "1");
check(10, "A");
check(15, "F");
check(16, "10");
check(65535, "FFFF");
check(65536, "10000");
check(11259375, "ABCDEF");
check(UINT64_MAX, "FFFFFFFFFFFFFFFF");
}[/src]


Zur Fehlerbehandlung: Wenn die Validierung von Benutzereingaben Teil der Funktion sein soll, dann bin ich bei Roin. Exceptions sind ein sinnvolles Mittel dafür. I.d.R. versuche ich, Validierung und Verarbeitung zu trennen. Das ist oft klarer lesbar und auch flexibler. Oft braucht man die Einzelteile, z.B. weil man dem Benutzer direkt in der UI Feedback geben will ohne gleich eine evtl. langwierige Berechnung anstoßen zu müssen. Insgesamt steckt da aber ein guter Batzen »kommt drauf an« drin.

Wie aktuell einen Error-State für ein ganzes Objekt würde ich jedenfalls vermeiden. Das kommt der globalen Error-Variable aus C recht nahe, und die ist notorisch fehleranfällig.

Noch zwei technische Sachen: Gruppen von const short als Flags zu verwenden, ist auch eher ein Pattern aus altem C. In C++ gibt’s dafür enum class. Komplette Großschreibung sollte man ausschließlich für Makros reservieren. Es gibt zwar die – aus meiner Sicht – Unsitte, Konstanten und Enumeratoren groß zu schreiben, aber das clasht früher oder später mit irgend einem Makro – v.a. beliebt unter Windows – und führt zu grauen Haaren und Heiserkeit vom vielen Fluchen.


Puh, das ist wieder mal deutlich länger geworden als geplant. Naja, ich vertraue einfach darauf, dass es noch nicht zu sehr tl;dr ist. :)
 
  • Thread Starter Thread Starter
  • #26
Einige Variblen deklarierst du am Funktionsanfang. Das kenne ich eher aus uraltem C-Code. Nachteil ist, dass notfalls uninitialisierte Variablen in einem viel größeren Scope verfügbar sind als nötig. Deswegen versuche, die Variablen idealerweise zusammen mit der ersten Verwendung zu deklarieren.
Mhm, okay, kann ich nachvollziehen. Ich dachte halt, dass es so übersichtlicher wäre. Aber dann bau ich das entsprechend um.

[*]sConv – ist unbenutzt, schau mal auf deine Compilerwarnungen.
[*]strTmp – kann sterben. Du kannst im switch direkt an strResult anhängen.
sConv hatte ich ja zuvor in Benutzung. Aber jetzt natürlich nicht mehr, dann kann das raus, da hast du Recht.

Die Potenzierung kann immer noch schief gehen. Dieser Ausdruck:
[...]
Richtig. Es war offenbar nur Glück, dass ich da bisher keine Fehler deswegen hatte. Oder aber die haben da bei Code::Blocks wirklich was an der Implementierung von pow() geändert. Wie dem auch sei, llround() ist so oder so ein guter Hinweis, das braucht man ja immer mal.

Btw: Bitte nie den klassischen C-Cast verwenden – also sowas wie (long long)foo –, denn der führt nicht zu einem Compilerfehler, auch wenn der Cast Unfug ist. Die C++-Casts sind immer besser: meistens static_cast<>, bei polymorphen Objekten auch mal dynamic_cast<>. Selten braucht man für Byte/Bit-Schubsereien auch mal reinterpret_cast<>. Aber Vorsicht damit: Das ist der, der die Compilerchecks abschaltet. Und Finger weg vom const_cast<>!
Und auch das ist natürlich ein super Tipp. In dem Buch, aus dem ich lerne, sind beide Formen (also der C- und der C++-Cast) aufgeführt, allerdings hatte ich sie dort als einigermaßen gleichwertig verstanden. Und die alte Variante ist natürlich weniger Schreibarbeit ;) Aaaber dann steige ich jetzt um auf die aktuellere Form :T

Zum Algorithmus selbst hat Roin schon in #4 eine feine Vereinfachung gepostet. Ich stell noch eine Implementierung daneben, die die Sache aus einer ganz anderen Richtung angeht.
Okay, den Codeschnipsel habe ich jetzt zwar vom Ansatz her verstanden, aber das ist glaube ich noch zu hoch für mich. Die Bit-Operatoren kommen später noch, soweit ich weiß :D

Wie aktuell einen Error-State für ein ganzes Objekt würde ich jedenfalls vermeiden. Das kommt der globalen Error-Variable aus C recht nahe, und die ist notorisch fehleranfällig.

Noch zwei technische Sachen: Gruppen von const short als Flags zu verwenden, ist auch eher ein Pattern aus altem C. In C++ gibt’s dafür enum class. Komplette Großschreibung sollte man ausschließlich für Makros reservieren. Es gibt zwar die – aus meiner Sicht – Unsitte, Konstanten und Enumeratoren groß zu schreiben, aber das clasht früher oder später mit irgend einem Makro – v.a. beliebt unter Windows – und führt zu grauen Haaren und Heiserkeit vom vielen Fluchen.

Mhm, okay... Dann gucke ich mir dafür vielleicht doch die Exceptions an. Ich denke nochmal drüber nach.
Die Enumerations kenne ich prinzipiell schon. War dann nur doch zu faul, die zu nutzen :p Baue ich dann auch noch um. Allerdings schreibt der Autor meines Buches tatsächlich, dass eine Konvention ist const-Bezeichner groß zu schreiben. Ist vielleicht auch einfach Geschmackssache, oder aber er kommt noch aus einer anderen Zeit, wo das üblicher war oder so.

--- [2018-09-11 20:24 CEST] Automatisch zusammengeführter Beitrag ---

So, ich glaube, ich habe soweit alles eingearbeitet. Mensch, da lerne ich ja noch richtig was :D :T
Habe leider eben festgestellt, dass für die Umwandlung von und in binär selbst der unsigned long long eher klein ist. Vielleicht sollte ich dafür doch String verwenden. Da überlege ich nochmal.

Vielen Dank auf jeden Fall schon für eure ganzen Ratschläge! Ist gut zu wissen, dass hier einige C++-Nasen an Board sind. Ich komme bestimmt mal wieder auf euch zurück :)
 
Zurück
Oben