Celem tego artykułu jest wyjaśnienie najważniejszych aspektów dotyczących listy inicjalizacyjej składowych (ang. member initializer list), przedstawiając przy tym problemy, jakie mogą wystąpić podczas ich implementacji. Nazwy tej listy nie należy mylić z nazwą typu std::initializer_list, który pojawił się w standardzie C++11, przy czym w dalszej części artykułu będę stosował jej skróconą (powszechną) nazwę, czyli ”lista inicjalizacyjna”. Artykuł podzielony został na dwie części. Pierwsza z nich przedstawia podstawowe informacje dotyczące list inicjalizacyjnych, natomiast druga przedstawia jakie możliwości w tym zakresie niosą ze sobą nowsze standardy języka C++ (od standardu C++11). W dalszej części artykułu będziemy posługiwać się następującym przykładem (z odpowiednimi modyfikacjami w zależności od przedstawianego zagadnienia):

#include <iostream>
#include <memory>

class NamedBuffer
{
public:
   NamedBuffer(char const *n, uint32_t s)
      : name(n)
      , size(s)
      , buffer(std::make_unique<uint8_t[]>(size))
   {
      this->info();
   }
   
   void info()
   {
      std::cout << "name : " << this->name << '\n';
      std::cout << "size : " << this->size << '\n';
   }
   
   // ...

private:
   std::string name;
   uint32_t size;
   std::unique_ptr<uint8_t[]> buffer;
   
   // ...
};

int main()
{
   constexpr char const *name = "TestBuffer";
   constexpr uint32_t size = 1024;
    
   NamedBuffer nb(name, size);
}

Uruchom w edytorze

name : TestBuffer                                                                                                                                                                           
size : 1024

Zastosowanie

Lista inicjalizacyjna składowych stanowi podstawę implementacji konstruktorówjęzyku C++. Jej zadaniem jest inicjalizacja niestatycznych pól składowych oraz podobiektów klas bazowych podczas konstrukcji obiektu głównego. Znaczenie listy inicjalizacyjnej jest na tyle istotne, iż w niektórych przypadkach jej stosowanie jest konieczne. Jednocześnie jej nieprawidłowe stosowanie może prowadzić m.in. do trudnych w debugowaniu błędów logicznych. Prosae implementacja konstruktora prowadzi często do sytuacji, w której konstruktor składa się wyłącznie z listy inicjalizacyjnej, natomiast jego ciało pozostaje puste. Bardziej skomplikowane operacje zostają wtedy oddelegowane do innych metod zaimplementowanych w danej klasie. Decydując się na bardziej złożony konstruktor należy mieć na uwadze m.in. następujące fakty:

  • konstruktorze może zostać rzucony wyjątek, co w efekcie spowoduje, że obiekt nie zostanie utworzony (może on być stworzony albo w całości albo wcale)
  • klasa jest trudniejsza w testowaniu, bowiem konstruktor nie zwraca niczego, a więc ogranicza to nasze możliwości w zakresie pisania testów jednostkowych (ang. unit tests)

Składnia

Implementacja listy inicjalizacyjnej rozpoczyna się od znaku ”:” (dwukropek) i poprzedza ciało konstruktora. Kolejne pozycje listy inicjalizacyjnej oddzielone są znakiem ”,” (przecinek). Obecność białych znaków takich jak spacja, tabulacja czy przejście do nowej linii nie mają na ogół znaczenia dla kompilatora, dlatego możemy spotkać się z różnymi zapisami listy inicjalizacyjnej. Poniżej przedstawiam kilka sposobów zapisu listy inicjalizacyjnej dla naszego przykładu:

#include <iostream>
#include <memory>

class NamedBuffer
{
public:
   // For really short initializer list
   // Too long for single line with buffer init
   NamedBuffer(char const *n, uint32_t s) : name(n), size(s),
                                            buffer(std::make_unique<uint8_t[]>(size))
   {
      // ...
   }
   
   // ...

};
#include <iostream>
#include <memory>

class NamedBuffer
{
public:
   NamedBuffer(char const *n, uint32_t s)
   // For really short initializer list
   : name(n), size(s), buffer(std::make_unique<uint8_t[]>(size))
   {
      // ...
   }
   
   // ...

};
#include <iostream>
#include <memory>

class NamedBuffer
{
public:
   NamedBuffer(char const *n, uint32_t s) : 
      name(n),
      size(s),
      // Append "," after adding new poistion
      buffer(std::make_unique<uint8_t[]>(size))
   {
      // ...
   }
   
   // ...

};
#include <iostream>
#include <memory>

class NamedBuffer
{
public:
   NamedBuffer(char const *n, uint32_t s)
      : name(n)
      , size(s)
      , buffer(std::make_unique<uint8_t[]>(size))
      // Simply add new position with preceding ","
   {
      // ...
   }
   
   // ...

};

Pierwsze dwa sposoby będą wystarczające w przypadku bardzo krótkich list inicjalizacyjnych składających się z jednej lub dwóch pozycji (takich jak namesize w naszym przykładzie). Trzeci sposób jest bardziej uniwersalny, jednakże dodając nową pozycję na końcu listy inicjalizacyjnej, zawsze musimy pamiętać aby po poprzedniej (dotychczas ostatniej) pozycji wstawić przecinek. Osobiście preferuje ostatni przedstawiony sposób, bowiem dodanie nowej pozycji nie wymaga modyfikacji poprzedniej pozycji o dodatkowy przecinek (pamiętając przy tym, iż nowa pozycje zawsze należy poprzedzić przecinkiem).

Wskaźnik this

Zmodyfikujmy nieco nasz przykład zmieniając nazwy parametrów na takie same jak nazwy pól składowych:

#include <iostream>
#include <memory>

class NamedBuffer
{
public:
   NamedBuffer(char const *name, uint32_t size)
      : name(name)
      , size(size)
      , buffer(std::make_unique<uint8_t[]>(size))
   {
      this->info();
   }
   
   void info()
   {
      std::cout << "name : " << this->name << '\n';
      std::cout << "size : " << this->size << '\n';
   }
   
   // ...

private:
   std::string name;
   uint32_t size;
   std::unique_ptr<uint8_t[]> buffer;
   
   // ...
};

int main()
{
   constexpr char const *name = "TestBuffer";
   constexpr uint32_t size = 1024;
    
   NamedBuffer nb(name, size);
}

Uruchom w edytorze

name : TestBuffer
size : 1024

Powyższy kod źródłowy wykonana się poprawnie. Należy jednak mieć na uwadze, że nazwy namesize odnoszą się teraz do nazw parametrów. Dotyczy to zarówno ciała konstruktora, jak i wyrażeń inicjalizujących elementy na liście inicjalizacyjnej (na przykład składowa buffer z naszego przykładu jest teraz inicjalizowana z wykorzystaniem parametru size, a nie składowej size). Dzieje się tak ponieważ zakres parametru jest węższy (ogranicza się jedynie do konstruktora) niż w przypadku składowej (zakres całej klasy NamedBuffer). Problem przesłaniania nazw (ang. shadowing) nie dotyczy inicjalizacji składowych o tych samych nazwach co nazwy parametrów (składową name inicjalizujemy parametrem name, a składową size inicjalizujemy parametrem size). Sytuacja wygląda inaczej w przypadku ciała konstruktora, bowiem aby skorzystać ze składowej (przesłoniętej przez parametr o tej samej nazwie), należy jawnie posłużyć się wskaźnikiem this. Aby to zademonstrować usuńmy inicjalizację składowych namesizelisty inicjalizacyjnej i przypiszmy odpowiadające im parametry w ciele konstruktora:

#include <iostream>
#include <memory>

class NamedBuffer
{
public:
   NamedBuffer(char const *name, uint32_t size)
      : buffer(std::make_unique<uint8_t[]>(size))
   {
      this->name = name; // assign "name" parameter to "name" member
      this->size = size; // assign "size" parameter to "size" member
       
      this->info();
   }
   
   void info()
   {
      std::cout << "name : " << this->name << '\n';
      std::cout << "size : " << this->size << '\n';
   }
   
   // ...

private:
   std::string name;
   uint32_t size;
   std::unique_ptr<uint8_t[]> buffer;
   
   // ...
};

int main()
{
   constexpr char const *name = "TestBuffer";
   constexpr uint32_t size = 1024;
    
   NamedBuffer nb(name, size);
}

Uruchom w edytorze

name : TestBuffer
size : 1024

Przedstawiony powyżej kod źródłowy ponownie wykona się poprawnie ale w sposób mniej efektywny. O  ile w przypadku typów fundamentalnych (takich jak typy całkowitoliczbowe reprezentowane tutaj przez typ uint32_t) nie stanowi to większego problemu, to w przypadku typów bardziej złożonych może to już negatywnie odbić się na wydajności aplikacji. Jako przykład weźmiemy tutaj składową name, która jest typu std::string (plik nagłówkowy string). W przykładzie opartym na liście inicjalizacyjnej składową tą tworzymy wprost przekazując do jej konstruktora parametr typu char const * (std::string::string(const char *)). Tymczasem w inicjalizacji zaimplementowanej w ciele konstruktora wykonujemy dwa kroki:

  • składowa name tworzona jest niejawnie z wykorzystaniem konstruktora domyślnego (łańcuch tekstowy przez nią przechowywany jest pusty)
  • do obiektu (składowej) utworzonego w pierwszym kroku przypisany zostaje łańcuch tekstowy, reprezentowany przez parametr name (typu const char *) – odbywa się to w sposób jawny w ciele konstruktora z wykorzystaniem operatora przypisania przyjmującego parametr typu const char * ( std::string::operator=(const char *s))

Inaczej mówiąc, zawsze preferuj inicjalizację w obrębie listy inicjalizacyjnej zamiast w ciele konstruktora.

Kolejność inicjalizacji

Zmodyfikujmy nasz oryginalny przykład zamieniając kolejność deklaracji składowych sizebuffer:

#include <iostream>
#include <memory>

class NamedBuffer
{
public:
   NamedBuffer(char const *n, uint32_t s)
      : name(n)
      , size(s)
      , buffer(std::make_unique<uint8_t[]>(size))
   {
      this->info();
   }
   
   void info()
   {
      std::cout << "name : " << this->name << '\n';
      std::cout << "size : " << this->size << '\n';
   }
   
   // ...

private:
   std::string name;
   std::unique_ptr<uint8_t[]> buffer;
   uint32_t size; // size memeber declared after buffer member
   
   // ...
};

int main()
{
   constexpr char const *name = "TestBuffer";
   constexpr uint32_t size = 1024;
    
   NamedBuffer nb(name, size);
}

Uruchom w edytorze

name : TestBuffer                                                                                                                                                                     
size : 1024

Pozornie wszystko wygląda w porządku. Aby się przekonać czy tak jest w rzeczywistości wykonajmy pewien eksperyment zmieniając typ składowej bufferstd::unique_ptr (plik nagłówkowy memory) na std::vector (plik nagłówkowy vector), dzięki czemu będziemy mogli pobrać rozmiar tak powstałego bufora (metodą std::vector::size()):

#include <iostream>
#include <vector>

class NamedBuffer
{
public:
   NamedBuffer(char const *n, uint32_t s)
      : name(n)
      , size(s)
      , buffer(size)
   {
      this->info();
   }
   
   void info()
   {
      std::cout << "name : " << this->name << '\n';
      std::cout << "size : " << this->size << '\n';
      std::cout << "vsize: " << this->buffer.size() << '\n';
   }
   
   // ...

private:
   std::string name;
   std::vector<uint8_t> buffer;
   uint32_t size; // size memeber declared after buffer member
   
   // ...
};

int main()
{
   constexpr char const *name = "TestBuffer";
   constexpr uint32_t size = 1024;
    
   NamedBuffer nb(name, size);
}

Uruchom w edytorze

name : TestBuffer                                                                                                                                                                     
size : 1024                                                                                                                                                                           
vsize: 1933947456

W rzeczywistości mamy tu do czynienia z poważnym błędem logicznym, bowiem otrzymaliśmy bufor o losowym rozmiarze (niedeterministyczne zachowanie). Dzieje się tak ponieważ w momencie tworzenia obiektu buffer, składowa size przekazywna do jego konstruktora nie jest jeszcze zainicjalizowna. Inicjalizacja składowej size następuje dopiero po inicjalizacji składowej buffer, co jest zgodne z kolejnością deklaracji składowychklasie (a nie kolejnością inicjalizacji na liście inicjalizacyjnej). Należy więc zwracać szczególną uwagę na to, aby kolejność deklaracji składowychklasie oraz ich inicjalizacji na liście inicjalizacyjnej konstruktora była taka sama, nawet jeśli problem taki można rozwiązać inicjalizując składową buffer bezpośrednio za pomocą parametru s:

#include <iostream>
#include <vector>

class NamedBuffer
{
public:
   NamedBuffer(char const *n, uint32_t s)
      : name(n)
      , size(s)
      , buffer(s)
   {
      this->info();
   }
   
   void info()
   {
      std::cout << "name : " << this->name << '\n';
      std::cout << "size : " << this->size << '\n';
      std::cout << "vsize: " << this->buffer.size() << '\n';
   }
   
   // ...

private:
   std::string name;
   std::vector<uint8_t> buffer;
   uint32_t size; // size memeber declared after buffer member
   
   // ...
};

int main()
{
   constexpr char const *name = "TestBuffer";
   constexpr uint32_t size = 1024;
    
   NamedBuffer nb(name, size);
}

Uruchom w edytorze

name : TestBuffer                                                                                                                                                                     
size : 1024                                                                                                                                                                           
vsize: 1024

Możemy zmodyfikować nasz kod źródłowy tak, aby zredukować ilość składowych, a tym samych zmniejszyć rozmiar listy inicjalizacyjnej. W powyższym przykładzie możemy wyeliminować składową size, bowiem rozmiar ten przechowywany jest bezpośrednio w składowej buffer (typu std::vector):

#include <iostream>
#include <vector>

class NamedBuffer
{
public:
   NamedBuffer(char const *n, uint32_t s)
      : name(n)
      , buffer(s)
   {
      this->info();
   }
   
   void info()
   {
      std::cout << "name : " << this->name << '\n';
      std::cout << "size : " << this->buffer.size() << '\n';
   }
   
   // ...

private:
   std::string name;
   std::vector<uint8_t> buffer;
   // no size member
   
   // ...
};

int main()
{
   constexpr char const *name = "TestBuffer";
   constexpr uint32_t size = 1024;
    
   NamedBuffer nb(name, size);
}

Uruchom w edytorze

name : TestBuffer                                                                                                                                                                     
size : 1024

Inicjalizacja domyślna

Tym razem zmodyfikujmy nasz oryginalny przykład usuwając inicjalizację składowych namesize z listy inicjalizacyjnej:

#include <iostream>
#include <memory>

class NamedBuffer
{
public:
   NamedBuffer(char const *n, uint32_t s)
      // no name and size members
      : buffer(std::make_unique<uint8_t[]>(size))
   {
      this->info();
   }
   
   void info()
   {
      std::cout << "name : " << this->name << '\n';
      std::cout << "size : " << this->size << '\n';
   }
   
   // ...

private:
   std::string name;
   uint32_t size;
   std::unique_ptr<uint8_t[]> buffer;
   
   // ...
};

int main()
{
   constexpr char const *name = "TestBuffer";
   constexpr uint32_t size = 1024;
    
   NamedBuffer nb(name, size);
}

Uruchom w edytorze

name :                                                                                                                                                                                
size : 4196880

Otrzymaliśmy pusty łańcuch tekstowy dla składowej name oraz losową wartość dla składowej size. Typem składowej size jest uint32_t, który jako typ całkowity jest oczywiście typem fundamentalnym, który jednocześnie jest typem skalarnym (do tej kategorii należą przede wszystkim typy fundamentalnetypy skalarne). Jest to przykład typu, dla którego składowe nie są inicjalizowane podczas konstrukcji obiektu głównego (przede wszystkim nie jest to klasa, czyli typ posiadający konstruktor). W naszym przypadku objawia się to losową wartością dla składowej size (nie jest to więc spodziewana wartość 0) i prowadzi to do podobnego problemu (błędu logicznego wynikającego z niedeterministycznej wartości składowej size) jaki wystąpił przy zamienionej kolejności deklaracji składowych sizebuffer.

Sytuacja wygląda zupełnie inaczej w przypadku składowej name, której typem jest std::string. Klasa std::string ma jawnie zdefiniowany konstruktor domyślny, który jest wywoływany w trakcie konstruowania obiektu głównego, a efektem jego działania jest pusty łańcuch tekstowy, czyli tak jak ma to miejsce w przypadku składowej name.

Niezależnie od typu składowej możemy ją jawnie inicjalizować na liście inicjalizacyjnej za pomocą pustych nawiasów (bez wyrażenia inicjalizującego):

#include <iostream>
#include <vector>

class NamedBuffer
{
public:
   NamedBuffer(char const *n, uint32_t s)
      : name() // deafult init of name member
      , size() // default init of size member
      , buffer(size)
   {
      this->info();
   }
   
   void info()
   {
      std::cout << "name : " << this->name << '\n';
      std::cout << "size : " << this->size << '\n';
      std::cout << "vsize: " << this->buffer.size() << '\n';
   }
   
   // ...

private:
   uint32_t size;
   std::string name;
   std::vector<uint8_t> buffer;
   
   // ...
};

int main()
{
   constexpr char const *name = "TestBuffer";
   constexpr uint32_t size = 1024;
    
   NamedBuffer nb(name, size);
}

Uruchom w edytorze

name :                                                                                                                                                                                
size : 0                                                                                                                                                                              
vsize: 0

Klasy bazowe

Załóżmy teraz, że chcemy rozszerzyć funkcjonalność klasy NamedBuffer. W tym celu możemy zaimplementować nową klasę, która będzie dziedziczyć publicznie po klasie NamedBuffer. Ponadto chcemy aby obiekt takiej klasy nie mógł być ani przenoszony ani kopiowany. W tym celu możemy zaimplementować klasę NonMovableCopyable, po której będziemy dziedziczyć prywatnie:

class NonMovableAndCopyable
{
protected:
   NonMovableAndCopyable()
   {
      std::cout << "NonMovableAndCopyable::NonMovableAndCopyable()\n";  
   }
   
   ~NonMovableAndCopyable() = default;
   
   NonMovableAndCopyable & operator=(NonMovableAndCopyable &&) = delete;
};

W efekcie powstanie klasa ExclusiveNamedBuffer:

#include <iostream>
#include <vector>

class NonMovableAndCopyable
{
protected:
   NonMovableAndCopyable()
   {
      std::cout << "NonMovableAndCopyable::NonMovableAndCopyable()\n";  
   }
   
   ~NonMovableAndCopyable() = default;
   
   NonMovableAndCopyable & operator=(NonMovableAndCopyable &&) = delete;
};

class NamedBuffer
{
public:
   NamedBuffer(char const *name, uint32_t size)
      : name(name)
      , buffer(size)
   {
      this->info();
      std::cout << "NamedBuffer::NamedBuffer()\n"; 
   }
   
   void info()
   {
      std::cout << "name : " << this->name << '\n';
      std::cout << "size : " << this->buffer.size() << '\n';
   }
   
   // ...

private:
   std::string name;
   std::vector<uint8_t> buffer;
   
   // ...
};


class ExclusiveNamedBuffer : public NamedBuffer, private NonMovableAndCopyable
{
public:
   ExclusiveNamedBuffer(char const *name, uint32_t size)
      : NamedBuffer(name, size)
   {
      // ...
   }
    
   // ...    
};

int main()
{
   constexpr char const *name = "TestBuffer";
   constexpr uint32_t size = 1024;
    
   ExclusiveNamedBuffer enb(name, size);
   
   // ExclusiveNamedBuffer cenb = enb;            // non-copyable
   // ExclusiveNamedBuffer menb = std::move(enb); // non-movable 
}

Uruchom w edytorze

name : TestBuffer                                                                                                                                                                     
size : 1024                                                                                                                                                                           
NamedBuffer::NamedBuffer()                                                                                                                                                            
NonMovableAndCopyable::NonMovableAndCopyable()

Jak widać inicjalizacja podobiektów klas bazowych na liście inicjalizacyjnej jest analogiczna jak w przypadku pól składowych. Jeżeli nie wyszczególnimy klasy na liście inicjalizacyjnej, wówczas wywołany zostanie jej konstruktor domyślny, tak jak ma to miejsce w przypadku klasy bazowej NonMovableAndCopyable. Oczywiście nic nie stoi na przeszkodzie aby jawnie wyspecyfikować domyślne tworzenie podobiektu klasy bazowej (podobnie jak w przypadku domślnej inicjalizacji pól składowych stosujemy puste nawiasy):

#include <iostream>
#include <vector>

class NonMovableAndCopyable
{
protected:
   NonMovableAndCopyable()
   {
      std::cout << "NonMovableAndCopyable::NonMovableAndCopyable()\n";  
   }
   
   ~NonMovableAndCopyable() = default;
   
   NonMovableAndCopyable & operator=(NonMovableAndCopyable &&) = delete;
};

class NamedBuffer
{
public:
   NamedBuffer(char const *name, uint32_t size)
      : name(name)
      , buffer(size)
   {
      this->info();
      std::cout << "NamedBuffer::NamedBuffer()\n"; 
   }
   
   void info()
   {
      std::cout << "name : " << this->name << '\n';
      std::cout << "size : " << this->buffer.size() << '\n';
   }
   
   // ...

private:
   std::string name;
   std::vector<uint8_t> buffer;
   
   // ...
};


class ExclusiveNamedBuffer : public NamedBuffer, private NonMovableAndCopyable
{
public:
   ExclusiveNamedBuffer(char const *name, uint32_t size)
      : NamedBuffer(name, size)
      , NonMovableAndCopyable() // default constructed base class subobject 
   {
      // ...
   }
    
   // ...    
};

int main()
{
   constexpr char const *name = "TestBuffer";
   constexpr uint32_t size = 1024;
    
   ExclusiveNamedBuffer enb(name, size);
   
   // ExclusiveNamedBuffer cenb = enb;            // non-copyable
   // ExclusiveNamedBuffer menb = std::move(enb); // non-movable 
}

Uruchom w edytorze

name : TestBuffer                                                                                                                                                                     
size : 1024                                                                                                                                                                           
NamedBuffer::NamedBuffer()                                                                                                                                                            
NonMovableAndCopyable::NonMovableAndCopyable()

Odwróćmy teraz kolejność inicjalizacji podobiektów klas bazowych na liście inicjalizacyjnej:

#include <iostream>
#include <vector>

class NonMovableAndCopyable
{
protected:
   NonMovableAndCopyable()
   {
      std::cout << "NonMovableAndCopyable::NonMovableAndCopyable()\n";  
   }
   
   ~NonMovableAndCopyable() = default;
   
   NonMovableAndCopyable & operator=(NonMovableAndCopyable &&) = delete;
};

class NamedBuffer
{
public:
   NamedBuffer(char const *name, uint32_t size)
      : name(name)
      , buffer(size)
   {
      this->info();
      std::cout << "NamedBuffer::NamedBuffer()\n"; 
   }
   
   void info()
   {
      std::cout << "name : " << this->name << '\n';
      std::cout << "size : " << this->buffer.size() << '\n';
   }
   
   // ...

private:
   std::string name;
   std::vector<uint8_t> buffer;
   
   // ...
};


class ExclusiveNamedBuffer : public NamedBuffer, private NonMovableAndCopyable
{
public:
   ExclusiveNamedBuffer(char const *name, uint32_t size)
      // Reverse order
      : NonMovableAndCopyable()
      , NamedBuffer(name, size)
      
   {
      // ...
   }
    
   // ...    
};

int main()
{
   constexpr char const *name = "TestBuffer";
   constexpr uint32_t size = 1024;
    
   ExclusiveNamedBuffer enb(name, size);
   
   // ExclusiveNamedBuffer cenb = enb;            // non-copyable
   // ExclusiveNamedBuffer menb = std::move(enb); // non-movable 
}

Uruchom w edytorze

name : TestBuffer                                                                                                                                                                     
size : 1024                                                                                                                                                                           
NamedBuffer::NamedBuffer()                                                                                                                                                            
NonMovableAndCopyable::NonMovableAndCopyable()

Okazuje się, że kolejność tworzenia podobiektów klas bazowych jest zgodna z kolejnością ich wystąpienia na liście dziedziczenia, a więc nie zależy od kolejności wystąpienia na liście inicjalizacyjnej. Jest to więc sytuacja analogiczna jak w przypadku pól składowych, które jak już wiemy inicjalizowane są zgodnie z kolejnościę ich deklaracjiklasie.

Składowe stałe i referencyjne

Tym razem załóżmy, że chcemy aby rozmiar naszego bufora pozostał niezmienny. W tym celu do typu składowej size (uint32_t) należy dodać słowo kluczowe const:

#include <iostream>
#include <memory>

class NamedBuffer
{
public:
   NamedBuffer(char const *n, uint32_t s)
      : name(n)
      , size(s)
      , buffer(std::make_unique<uint8_t[]>(size))
   {
      this->info();
   }
   
   void info()
   {
      std::cout << "name : " << this->name << '\n';
      std::cout << "size : " << this->size << '\n';
   }
   
   // ...

private:
   std::string name;
   const uint32_t size; // size member is const
   std::unique_ptr<uint8_t[]> buffer;
   
   // ...
};

int main()
{
   constexpr char const *name = "TestBuffer";
   constexpr uint32_t size = 1024;
    
   NamedBuffer nb(name, size);
}

Uruchom w edytorze

name : TestBuffer                                                                                                                                                                     
size : 1024

Ususnięcie składowej sizelisty inicjalizacyjnej spowoduje błąd kompilacji, bowiem mamy tu do czynienia z typem fundamentalnym:

#include <iostream>
#include <memory>

class NamedBuffer
{
public:
   NamedBuffer(char const *n, uint32_t s)
      : name(n)
      // no size member
      , buffer(std::make_unique<uint8_t[]>(size))
   {
      this->info();
   }
   
   void info()
   {
      std::cout << "name : " << this->name << '\n';
      std::cout << "size : " << this->size << '\n';
   }
   
   // ...

private:
   std::string name;
   const uint32_t size; // size member is const
   std::unique_ptr<uint8_t[]> buffer;
   
   // ...
};

int main()
{
   constexpr char const *name = "TestBuffer";
   constexpr uint32_t size = 1024;
    
   NamedBuffer nb(name, size);
}
main.cpp: In constructor ‘NamedBuffer::NamedBuffer(const char*, uint32_t)’:
main.cpp:7:4: error: uninitialized const member in ‘const uint32_t {aka const unsigned int}’ [-fpermissive]
    NamedBuffer(char const *n, uint32_t s)
    ^~~~~~~~~~~
main.cpp:25:19: note: ‘const uint32_t NamedBuffer::size’ should be initialized
    const uint32_t size; // size member is const
                   ^~~~

Sytuacja wygląda zupełnie inaczej w przypadku typów (klas), które posiadają jawnie zdefiniowany konstruktor domyślny, bowiem ponownie (tak jak w przypadku składowej, która nie jest stałą) otrzymamy domyślną konstrukcję składowej name, której efektem będzie pusty łańcuch tekstowy:

#include <iostream>
#include <memory>

class NamedBuffer
{
public:
   NamedBuffer(char const *n, uint32_t s)
      // no name member
      : size(s)
      , buffer(std::make_unique<uint8_t[]>(size))
   {
      this->info();
   }
   
   void info()
   {
      std::cout << "name : " << this->name << '\n';
      std::cout << "size : " << this->size << '\n';
   }
   
   // ...

private:
   const std::string name; // name member is const
   uint32_t size;
   std::unique_ptr<uint8_t[]> buffer;
   
   // ...
};

int main()
{
   constexpr char const *name = "TestBuffer";
   constexpr uint32_t size = 1024;
    
   NamedBuffer nb(name, size);
}

Uruchom w edytorze

name :                                                                                                                                                                                
size : 1024

List inicjalizacyjnych należy używać bezwzglęðnie (niezależnie od typu składowej) w przypadku inicjalizacji składowych refrencyjnych. Załóżmy teraz, że poprzez konstruktor do naszego obiektu chcemy przekazać strukturę konfigurującą działanie bufora, przy czym odpowiadająca jej składowa będzie referencją do stałej (pozwalającą odwołać się do przekazanej struktury), dzięki czemu możemy uniknąć kopiowania potencjalnie dużej struktury danych:

#include <iostream>
#include <vector>

struct defult_configuration
{
   static constexpr int size_limit = 1024;
   
   // ...
};

template <typename config_t>
class NamedBuffer
{
public:
   NamedBuffer(char const *name, uint32_t size, config_t const & cfg)
      : config(cfg)
      , name(name)
      , buffer(size > config.size_limit ? config.size_limit : size)
      
   {
      this->info();
   }
   
   void info()
   {
      std::cout << "name : " << this->name << '\n';
      std::cout << "size : " << this->buffer.size() << '\n';
   }
   
   // ...

private:
   config_t const & config;
   std::string name;
   std::vector<uint8_t> buffer;
   
   // ...
};

int main()
{
   defult_configuration cfg; // Config structure
    
   constexpr char const *name = "TestBuffer";
   constexpr uint32_t size = 1024 * 1024; // Too big size for buffer
    
   NamedBuffer nb(name, size, cfg);
}

Uruchom w edytorze

name : TestBuffer                                                                                                                                                                     
size : 1024

Usunięcie inicjalizacji składowej referencyjnejlisty inicjalizacyjnej spowoduje oczywiście błąd kompilacji:

#include <iostream>
#include <vector>

struct defult_configuration
{
   static constexpr int size_limit = 1024;
   
   // ...
};

template <typename config_t>
class NamedBuffer
{
public:
   NamedBuffer(char const *name, uint32_t size, config_t const & cfg)
      : name(name)
      , buffer(size > cfg.size_limit ? cfg.size_limit : size)
      
   {
      this->info();
   }
   
   void info()
   {
      std::cout << "name : " << this->name << '\n';
      std::cout << "size : " << this->buffer.size() << '\n';
   }
   
   // ...

private:
   config_t const & config;
   std::string name;
   std::vector<uint8_t> buffer;
   
   // ...
};

int main()
{
   defult_configuration cfg; // Config structure
    
   constexpr char const *name = "TestBuffer";
   constexpr uint32_t size = 1024 * 1024; // Too big size for buffer
    
   NamedBuffer nb(name, size, cfg);
}
main.cpp: In instantiation of ‘NamedBuffer<config_t>::NamedBuffer(const char*, uint32_t, const config_t&) [with config_t = defult_configuration; uint32_t = unsigned int]’:
main.cpp:46:34:   required from here
main.cpp:15:4: error: uninitialized reference member in ‘const struct defult_configuration&’ [-fpermissive]
    NamedBuffer(char const *name, uint32_t size, config_t const & cfg)
    ^~~~~~~~~~~
main.cpp:32:21: note: ‘const defult_configuration& NamedBuffer::config’ should be initialized
    config_t const & config;
                     ^~~~~~

Rzucanie wyjątków

Zmieńmy typ parametru sizeuint32_t na uint64_t, a następnie spróbujmy utworzyć bufor o rozmiarze równym maksymalnej wartości przechowywyanej w typie uint64_t:

#include <iostream>
#include <vector>
#include <limits>

class NamedBuffer
{
public:
   NamedBuffer(char const *n, uint64_t s)
      : name(n)
      , buffer(s)
   {
      this->info();
   }
   
   void info()
   {
      std::cout << "name : " << this->name << '\n';
      std::cout << "size : " << this->buffer.size() << '\n';
   }
   
   // ...

private:
   std::string name;
   std::vector<uint8_t> buffer;
   
   // ...
};

int main()
{
   constexpr char const *name = "TestBuffer";
   constexpr uint64_t size = std::numeric_limits<uint64_t>::max();
    
   NamedBuffer nb(name, size);
}
terminate called after throwing an instance of 'std::bad_alloc'                                                                                                                       
  what():  std::bad_alloc

Okazuje się, że otrzymaliśmy błąd, sygnalizowany nieprzechwyconym wyjątkiem typu std::bad_alloc (dziedziczy po klasie std::exception), ponieważ nie było możliwe zaalokowaniej tak dużej ilości pamięci. W języku C++ mamy możliwość objęcia blokiem try-catch ciała konstruktoralisty inicjalizacyjnej , dzięki czemu wyjątek możemy obsłużyć w obrębie klasy:

#include <iostream>
#include <vector>
#include <limits>

class NamedBuffer
{
public:
   NamedBuffer(char const *n, uint64_t s)
   try
      : name(n)
      , buffer(s)
   {
      this->info();
   }
   catch (std::exception const & e)
   {
      std::cout << e.what() << '\n';
      exit(EXIT_FAILURE);
   }

   
   void info()
   {
      std::cout << "name : " << this->name << '\n';
      std::cout << "size : " << this->buffer.size() << '\n';
   }
   
   // ...

private:
   std::string name;
   std::vector<uint8_t> buffer;
   
   // ...
};

int main()
{
   constexpr char const *name = "TestBuffer";
   constexpr uint64_t size = std::numeric_limits<uint64_t>::max();
  
   NamedBuffer nb(name, size);
}

Uruchom w edytorze

std::bad_alloc

Podsumowanie

  • Listy inicjalizacyjne stanowią podstawę implementacji konstruktorów
  • Zadaniem listy inicjalizacyjnej jest inicjalziacja pół składowych klasy oraz podobiektów klas bazowych
  • Nazwy parametów konstrukotra mogą być takie same jak nazwy inicjalizowanych przez nie składowych (w pozstałych przypadkach nazwa składowej jest przesłaniana przez nazwę parametru i wówczas aby się do niej odwołać należy jawnie korzystać ze wskaźnika this)
  • Inicjalizacja pól składowych przebiega zgodnie z kolejnością ich deklaracjiklasie i nie zależy od kolejności ich wystąpienia na liście inicjalziacyjnej
  • Inicjalizacja podobiektów klas bazowych przebiega zgodnie z kolejnością ich wystąpienia na liście dziedziczenia i nie zależy od kolejności ich wystąpienia na liście inicjalziacyjnej
  • Typy fundamentalnetypy wskaźnikowe (typy skalarne) nie zostaną zainicjalizowane bez jawnego umieszenia ich na liście inicjalizacyjnej
  • Składowe stałe dla typów, które nie posiadają jawnie zdefiniowanego domyślnego konstruktora, muszą być zawsze inicjalizowane na liście inicjalizacyjnej
  • Składowe referencyjne muszą być zawsze inicjalizowane na liście inicjalizacyjnej
  • Na liście inicjalizacyjnej można jawnie inicjalizować składowe (puste nawiasy bez wyrażenia inicjalizującego), które normalnie mogą być inicjalizowane domyślie (bez jawnego udziału listy inicjalizacyjnej)
  • Listę inicjalizacyjną (razem z ciałem konstruktora) można objąć blokem try-catch