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); }
name : TestBuffer size : 1024
Zastosowanie
Lista inicjalizacyjna składowych stanowi podstawę implementacji konstruktorów w ję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:
- w 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 name i size 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); }
name : TestBuffer size : 1024
Powyższy kod źródłowy wykonana się poprawnie. Należy jednak mieć na uwadze, że nazwy name i size 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 name i size z listy 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); }
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 size i buffer:
#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); }
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 buffer z std::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); }
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ładowych w klasie (a nie kolejnością inicjalizacji na liście inicjalizacyjnej). Należy więc zwracać szczególną uwagę na to, aby kolejność deklaracji składowych w klasie 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); }
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); }
name : TestBuffer size : 1024
Inicjalizacja domyślna
Tym razem zmodyfikujmy nasz oryginalny przykład usuwając inicjalizację składowych name i size 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); }
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 fundamentalne i typy 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 size i buffer.
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); }
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 }
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 }
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 }
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 deklaracji w klasie.
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); }
name : TestBuffer size : 1024
Ususnięcie składowej size z listy 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); }
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); }
name : TestBuffer size : 1024
Usunięcie inicjalizacji składowej referencyjnej z listy 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 size z uint32_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 konstruktora i listy 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); }
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 deklaracji w klasie 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 fundamentalne i typy 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