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

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
#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.