
Kleine Fallstricke
Wollen wir also mal sehen,
was wir mit unserem
sum3
so alles anstellen können ...
Impliziter Typ eines Literals für init
int a[] = { 0, 1, 2, 3 };
float b[] = { 0.5, 1.5, 2.5, 3.5 };
int sa = sum3(a, a+4, 0); // Yeah!
float sb = sum3(b, b+4, 0); // Ouch!
Das Ergebnis sb
ist nicht ganz das erwartete ... was ist passiert?
Eigentlich das gleiche wie im Beispiel mit den strings:
Der Compiler bestimmt den Typ des Literals 0
nicht als float
(warum auch?),
sondern als int
.
Und wenn man Pech hat, sieht man auch keine Warnung, nirgends ...
(der gcc warnt nicht mit -Wall
, man muss -Wconversion
bemühen).
Also müssen wir wieder explizit werden:
float b[] = { 0.5, 1.5, 2.5, 3.5 };
float sb = sum3(b, b+4, float(0)); // Like this!
float sb1 = sum3(b, b+4, 0.0); // Or like that?
Das ist aber unschön und eine ergiebige Fehlerquelle.
(Ist übrigens 0.0
jetzt vom Typ float
oder double
?
float(0)
spricht dagegen eine eindeutige Sprache und ist verallgemeinerbar.)
Diese Fallstricke wollen wir uns bzw. unseren Nutzern doch gerne ersparen
— dazu gleich mehr. Doch zunächst …
Steuerungsmöglichkeiten durch init
Auf der anderen Seite ermöglicht die explizite Angabe des Ergebnis-Typs aber auch algorithmische Verbesserungen, indem ausdrücklich ein erweiterter Werte-Typ gewählt wird. Betrachten wir folgendes Beispiel:
vector<unsigned char> dice_rolls(BigN);
// Ueberraschung
unsigned sum_dice_rolls = sum3(dice_rolls.begin(), dice_rolls.end(), (unsigned char)(0));
Hier erleben wir eine unliebsame Überraschung, wenn wir uns das Ergebnis anschauen.
Der Typ unsigned char
kann problemlos das Ergebnis eines einzigen Würfelwurfs enthalten,
aber wenn N > 42
ist, dann kann die Summe schon zu groß sein:
Der Wertetyp der Sequenz ist für die Summation zu eng!
In diesem Fall wäre der Aufruf
unsigned sum_dice_rolls = sum3(dice_rolls.begin(), dice_rolls.end(), 0);
sogar (zufälligerweise) besser gewesen.
Etwas expliziter könnten wir schreiben
(wenn wir sicher sind, dass unsigned int
groß genug ist):
unsigned sum_dice_rolls = sum3(dice_rolls.begin(), dice_rolls.end(), unsigned(0));
Selbst wenn das Endergebnis in den nominellen Wertetyp der Sequenz hineinpasst, kann es nötig sein, zur Berechnung einen erweiterten Typ heranzuziehen. Das ist insbesondere bei Fließkomma-Berechnungen der Fall. Generell sollte man hier Berechnungen immer mit doppelter Genauigkeit ausführen:
std::vector<float> a(1000000);
// ...
double a_sum = sum3(a.begin(), a.end(), double(0.0));
Design-Verbesserungen ...
Als Quintessenz unserer Überlegungen folgt,
dass die explizite Auswahlmöglichkeit des Rechen-Wertetyp über den init
-Parameter
wichtige Steuerungsmöglichkeiten eröffnet, jedoch auch fehleranfällig ist.
Also sollten wir überlegen, wie wir die Fehlerquellen ausschalten,
aber die mit init
verbundenen Möglichkeiten erhalten.
Die Fehler bei der Benutzung von sum3
kommen im wesentlichen aus 2 Richtungen:
- Die implizite Angabe des Typs
von
init
durch ein Literal wie0
, was zu einem ungeeigneten Wertetyp führen kann - Die Verwendung eines zu kleinen Wertetyps für die Berechnungen und das Ergebnis
Fehlerquelle 1 ist in der Tat ein Problem,
das durch die generische Implementierung erzeugt wird.
Fehler 2 kann zwar auch durch Fehler 1 ausgelöst werden,
ist aber genauso bei expliziter Angabe des Typs von init
möglich
und kann (und wird!) auch in "handgestricktem" Code auftreten.
Wir kommen
später
auf Fehler vom Typ 2 zurück.
Um die 1. Fehlerquelle zu beseitigen, können wir z.B. die explizite Angabe des Typs erzwingen. Dazu ändern wir die Schnittstelle so, dass dieser Typ als Argument mitübergeben werden muss:
template<class T>
struct value {
typedef T type;
type val;
value(type const& t) : val(t) {}
// make T t = value<T>(v) possible
operator type() { return val; }
};
template<typename Iterator, typename T>
T
sum3b(Iterator a, Iterator a_end, value<T> init)
{
T res = init;
for(; a != a_end; ++a)
res += *a;
return res;
}
Damit müssen wir sum3b
jetzt so aufrufen:
float a[] = { 0.0, 0.5, 1.0, 1.5 };
float suma = sum3b(a, a+4, value<float>(0));
Das ist jetzt, zugegeben, etwas umständlicher. Wie sieht es denn mit den "normalen" Fällen aus, in denen der Wertetyp des Iterator und der Rechen-Wertetyp zusammenfallen können?