• Hallo liebe Userinnen und User,

    nach bereits längeren Planungen und Vorbereitungen sind wir nun von vBulletin auf Xenforo umgestiegen. Die Umstellung musste leider aufgrund der Serverprobleme der letzten Tage notgedrungen vorverlegt werden. Das neue Forum ist soweit voll funktionsfähig, allerdings sind noch nicht alle der gewohnten Funktionen vorhanden. Nach Möglichkeit werden wir sie in den nächsten Wochen nachrüsten. Dafür sollte es nun einige der Probleme lösen, die wir in den letzten Tagen, Wochen und Monaten hatten. Auch der Server ist nun potenter als bei unserem alten Hoster, wodurch wir nun langfristig den Tank mit Bytes vollgetankt haben.

    Anfangs mag die neue Boardsoftware etwas ungewohnt sein, aber man findet sich recht schnell ein. Wir wissen, dass ihr alle Gewohnheitstiere seid, aber gebt dem neuen Board eine Chance.
    Sollte etwas der neuen oder auch gewohnten Funktionen unklar sein, könnt ihr den "Wo issn da der Button zu"-Thread im Feedback nutzen. Bugs meldet ihr bitte im Bugtracker, es wird sicher welche geben die uns noch nicht aufgefallen sind. Ich werde das dann versuchen, halbwegs im Startbeitrag übersichtlich zu halten, was an Arbeit noch aussteht.

    Neu ist, dass die Boardsoftware deutlich besser für Mobiltelefone und diverse Endgeräte geeignet ist und nun auch im mobilen Style alle Funktionen verfügbar sind. Am Desktop findet ihr oben rechts sowohl den Umschalter zwischen hellem und dunklem Style. Am Handy ist der Hell-/Dunkelschalter am Ende der Seite. Damit sollte zukünftig jeder sein Board so konfigurieren können, wie es ihm am liebsten ist.


    Die restlichen Funktionen sollten eigentlich soweit wie gewohnt funktionieren. Einfach mal ein wenig damit spielen oder bei Unklarheiten im Thread nachfragen. Viel Spaß im ngb 2.0.

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

Mäxchen

lustiger Kumpane

Registriert
14 Juli 2013
Beiträge
294
Ort
am liebsten im Zelt
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.
 

Brother John

(schein)heilig
Veteran

Registriert
1 Aug. 2013
Beiträge
235
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.
 

Kenobi van Gin

Brillenschlange

Registriert
14 Juli 2013
Beiträge
3.620
Ort
.\
  • 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:

Roin

Freier Denker

Registriert
22 Juli 2013
Beiträge
581
@Brother John: 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.
 

Brother John

(schein)heilig
Veteran

Registriert
1 Aug. 2013
Beiträge
235
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. :)
 

Kenobi van Gin

Brillenschlange

Registriert
14 Juli 2013
Beiträge
3.620
Ort
.\
  • 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 :)
 
Oben