Initialisierungslisten
std::initializer_list bietet uns eine bequeme Möglichkeit, die neue unified
initialization Syntax auch mit Initialisierungslisten variabler Länge in eigenen
Datentypen zu benutzen. Dabei gilt es allerdings, einige Kleinigkeiten zu
beachten.
Video
Code
#include <iostream>
#include <vector>
#include <initializer_list>
template <typename T> class InfiniteSequence
{
std::vector<T> mValues;
typename std::vector<T>::const_iterator it;
public:
InfiniteSequence(T const &min, T const &max)
{
for (T i = min; i <= max; ++i) {
mValues.push_back(i);
}
it = mValues.begin();
}
InfiniteSequence(std::initializer_list<T> const &values)
{
for (auto i : values) {
mValues.push_back(i);
}
it = mValues.begin();
}
T const &get()
{
auto ret_it = it++;
if (it == mValues.end()) {
it = mValues.begin();
}
return *ret_it;
}
};
int main()
{
InfiniteSequence<int> inf { 1,6,3,2,9,10 };
InfiniteSequence<double> inf2(1.4, 5.7);
for (int i = 0; i < 20; ++i) {
std::cerr << inf.get() << " ";
}
std::cerr << std::endl;
for (int i = 0; i < 20; ++i) {
std::cerr << inf2.get() << " ";
}
std::cerr << std::endl;
}
Erklärung
Ziel der InfiniteSequence ist es, eine Sequenz von Werten unendlich oft zu
wiederholen. Immer, wenn einmal get() aufgerufen wird, soll der nächste Wert
aus der zugrundeliegenden Sequenz zurückgeliefert werden. Ist deren Ende
erreicht, beginnen wir wieder beim Anfang. Um das ganze bequem initialisieren zu
können, bietet sich die Syntax in Zeile 40 an: einfach eine
Initialisierungsliste beliebiger Länge angeben und fertig. C++11 bietet zur
Implementierung dieser Idee das Template std::initializer_list. Das ist eine
spezielle, typisierte Liste, die vom Compiler automatisch aus einer im Code
angegebenen Initialisierungsliste erzeugt wird. Man kann die durchlaufen und die
Werte auslesen – insofern verhält sie sich, wie andere Container-Klassen der
Standardbibliothek – aber nicht ändern. Objekte der Klasse sind auch nicht
direkt erzeugbar: nur die spezielle Initilisierungslistensyntax liefert Objekte
dieses Typs.
Im entsprechenden Konstruktor in Zeile 20 sieht man, wie die Liste durchlaufen
werden kann. Hier werden die enthaltenen Werte in den internen Wertevektor
unserer Klasse kopiert. Das wäre an sich nicht notwendig. Man könnte auch mit
mValues(values) bei der Initialisierung der Membervariablen machen.
std::vector bietet einen passenden Konstruktor an. Hier das Beispiel soll nur
der Verdeutlichung von std::initializer_list dienen.
Eine Besonderheit des Konstruktors mit der initializer_list: wenn zwei
Konstruktoren auf die übergebene Initialisierung passen (wie wir schon hatten:
mit der unified initialization können wir ja auch normale Konstruktoraufrufe mit
den geschweiften Klammern tun.), dann wird immer der Konstruktor mit der
initializer_list bevorzugt. Daher müssen wir in Zeile 41 die normale
Konstruktorsyntax mit runden Klammern verwenden, um klar zu machen, dass wir den
ersten Konstruktor aus Zeile 12 aufrufen wollen. Die "alte" Syntax ist also
nicht ausgestorben dank unified initialization. Es gibt immer noch Randfälle, wo
sie relevant ist.
Zwei Kleinigkeiten in der Klasse verdienen noch unsere Aufmerksamkeit: zum einen
das typename in Zeile 8. Hier haben wir das Problem, dass const_iterator ein
sogenannter "dependent type" ist, also ein Typ, dessen genaue Ausgestaltung von
der Struktur von std::vector<T> abhängt. An dieser speziellen Stelle dort ist
es syntaktisch nicht 100% klar, dass das wirklich ein Typ sein muss. Es könnte
auch eine statische Konstante oder Variable innerhalb von std::vector<T> sein.
In diesem Randfall (der so selten vorkommt, dass ich regelmäßig drüber stolpere,
wenn das passiert) müssen wir dem Compiler auf die Sprünge helfen, indem wir
typename davor setzen und damit anzeigen, dass const_iterator ein Typ ist.
Zum zweiten haben wir in Zeile 30 ein Konstrukt, wo it++ ausgeführt und das
Ergebnis zwischengespeichert wird. Hier gilt es zu beachten, dass das Postfix-++
ja den alten Wert vor dem Inkrement zurückliefert. Das brauche ich hier, damit
ich beim allerersten Aufruf von get() nicht das erste Element der Sequenz
überspringe. Eine Konstruktion, die man manchmal noch sieht, wäre in Zeile 34
folgende: return *(it++); Das hat quasi die gleiche Wirkung, ohne ret_it als
Variable zu benötigen. Ich finde das allerdings etwas unübersichtlich, weswegen
ich das Zwischenspeichern hier explizit gemacht habe.