Initializer list

Initialisatie versus toekenning

Om historische redenen zijn er twee manieren om een initialisatie te schrijven:

	int a = 5;      
	int b(6);       // "constructornotatie"
De eerste notatie ziet er eenvoudiger uit voor eenvoudige objecten zoals getallen, de tweede eerder voor complexere objecten: om een vector van 10 ints te maken is vector v(10) natuurlijker dan vector v=10;

Eigenlijk is de tweede notatie algemener: men roept immers de constructor op (je kunt primitieve objecten zoals int beschouwen als eenvoudige klassen)

Opgelet: de operator '=' wordt verder ook gebruikt voor een toekenning. Dit kan echter niet met de constructornotatie:

	int a = 5;      // initialisatie
	a = 7;          // toekenning

	int b(6);       // alternatieve schrijfwijze voor    int b = 6;
	b(8);           // FOUT

Initializer list

Beschouw volgende klasse:

class Complex {
	double re, im;                            // velden of attributen of leden
public:
	Complex(double re_, double im_) {         // constructor
		re = re_;
		im = im_;
	}
};
[Opmerking: het toevoegen van een underscore ('_') om onderscheid te maken tussen argumenten en velden (attributen) is een conventie. Soms voegt men vooraan een underscore toe voor de velden (_re). In Java zal men eerder this.re = re schrijven.]

Precies hetzelfde kan bekomen worden d.m.v een initializer list

class Complex {
	double re, im;                            
public:
	Complex(double re_, double im_) 
	  : re(re_), im(im_)              // initializer list
	  {}                              // constructor body
};
Men gebruikt dus de constructornotatie voor de initialisatie van de velden, maar dit voor de body van de constructor (tussen '{' en '}').

Zelfs indien men een initializer list gebruikt mag men toch iets in de constructor body zetten:

class Complex {
	double re, im;                            
public:
	Complex(double re_, double im_) : re(re_), im(im_) {
	    cout << re << " " << im << endl;
	}
};
In het algemeen is het zo dat het object (*this) volledig geconstrueerd is voordat de constructor body betreden wordt. Binnen de accolades zijn alle velden dus reeds gedefinieerd.

In bovenstaand geval kan men kiezen tussen beide notaties. Er zijn echter gevallen waarbij men een initializerlist moet gebruiken, namelijk indien bepaalde velden geen default constructor hebben, bij overerving van klassen zonder default constructor, en bij referentievelden.

Default constructor

Beschouw volgende klasse:
class Voertuig {
	double prijs;
public:
	Voertuig(double prijs_) { prijs = prijs_; }    // ofwel met initializer list
};

class Persoon1 {
	Voertuig voertuig;
public:
	Persoon1() : voertuig(1000) {}
	// Persoon1() {}                 // FOUT : kan voertuig niet construeren
};
In dit geval moet het veld voertuig d.m.v. een initializer list geconstrueerd worden, omdat Voertuig geen default constructor (= zonder argumenten) heeft.

Indien een veld wel een default constructor heeft, dan moet deze niet expliciet opgeroepen worden. Stel bv. dat je std::vector wil uitbreiden:

class MyVector {
	std::vector v;
public:
	MyVector() {}
	// equivalent: MyVector() : v() {}
	// equivalent: MyVector() : v(0) {}
};
De default constructor vector() maakt immers een lege vector aan.

De default constructor van primitieve types zoals int en double initializeren deze op nul!!
De eerste versie Complex (die zonder initializer list) is eigenlijk equivalent met

class Complex {
	double re, im;                            // velden of attributen
public:
	Complex(double re_, double im_)  : re(), im() {      // zet op nul!!  
		re = re_;                                        // geef waarde 
		im = im_;
	}
};
Een deftige compiler zal de initialisatie (op nul zetten) wegoptimaliseren. Niettemin zie je hier waarom het gebruik van een initialiser list verkieslijk is.

Overerving

Beschouw de klasse Vrachtwagen die erft van Voertuig (zie boven):

class Vrachtwagen : public Voertuig {   // erft 
	double laadvermogen;
public:
	Vrachtwagen(double prijs_, double laadvermogen_) 
	  : Voertuig(prijs), laadvermogen(laadvermogen_) {}
};
Bij constructie roept hij eerst de constructor van de basisklasse op (zoals super() in Java).

Indien de basisklasse een default constructor (=zonder argumenten) heeft, dan is het niet nodig deze expliciet op te roepen.

Referentievelden

class Persoon2 {
	Voertuig &voertuig;   // referentie naar een Voertuig
public:
	Persoon2(Voertuig& v) : voertuig(v) {}      
};
Elke referentie (reference) moet bij zijn definitie (contructie) "gebonden" worden aan een andere variabele. Dit geldt ook voor referentievelden in een klasse. Het weglaten van voertuig(v) resulteert in een compilatiefout.

Dit geldt niet voor pointers:

class Persoon2 {
	Voertuig *voertuig;   // referentie naar een Voertuig
public:
	Persoon2() {}      // voertuig = 0 !!
};
De pointer wordt op nul gezet.

Opmerking. Hoewel de defaultconstructor voor primitieve datatypes en pointers deze op nul zetten, is het niet aangewezen hierop te vertrouwen!! Zet liever expliciet velden op nul; dat is ook leesbaarder.

[W. Schepens sept. 2003]