W pierwszej części tego artykułu przedstawiłem podstawowe informacje dotyczące list inicjalizacyjnych składowych (ang. member initializer list). Są one wspólne dla wszystkich standardów języka C++, a więc także przed standardem C++11. W drugiej części omówię pewne cechy języka C++ związane z tematem list inicjalizacyjnych, które pojawiły się wraz z nowszymi standardami (poczynając od standardu C++11).

Inicjalizacja tablic

Nasze rozważania zacznijmy od następującego przykładu:

#include <iostream>
#include <cstdio>

template<uint32_t N>
class NamedFixedBuffer
{
public:
   NamedFixedBuffer(char const *n)
      : name(n)
      , size(N)
      , buffer()
   {
      this->info();
   }
   
   void info()
   {
      std::cout << "name : " << this->name << '\n';
      std::cout << "size : " << this->size << '\n';
   }
   
   void print()
   {
      for (uint32_t i = 0; i < this->size; ++i)
      {
         
         printf("0x%02X ", buffer[i]);
         
         // ...   
      }
   }
   
   // ...

private:
   std::string name;
   uint32_t size;
   uint8_t buffer[N];
   
   // ...
};

int main()
{
   constexpr char const *name = "TestBuffer";
   constexpr uint32_t size = 16;
    
   NamedFixedBuffer<size> nfb(name);
   nfb.print();
}

Uruchom w edytorze

name : TestBuffer                                                                                                                                                                           
size : 16                                                                                                                                                                                   
0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00

Naszą uwagę skupimy teraz na inicjalizacji składowej buffer. Tym razem jest to ”surowa” tablica w stylu języka C. Zawiera ona elementy typu fundamentalnego uint8_t (pojedynczy bajt bufora). Oznacza to, że musimy jawnie przeprowadzić jego domyślną inicjalizację poprzez wyspecyfikowanie go na liścię inicjalizacyjnej (stosując składnię z pustymi nawiasami). W efekcie otrzymamy bufor wypełniony zerami (w przypadku braku jawnej specyfikacji na liście inicjalizacyjnej będą to niezdefiniowane wartości losowe). Jest to podejście prawidłowe także przed standardem C++11.

Sytuacja skomplikuje się w przypadku gdy będziemy chcieli zainicjalizować składową tablicową pewnymi konkretnymi wartościami. W przypadku standardu starszego niż C++11 taką inicjalizację musimy przeprowadzić w ciele konstruktora. Załóżmy więc, że musimy zainicjalizować pierwszy bajt naszego bufora, a pozostałe bajty pozostawić wyzerowane:

#include <iostream>
#include <cstdio>

template<uint32_t N>
class NamedFixedBuffer
{
    static_assert(N > 0, "Buffer must have non-zero size");
public:
   NamedFixedBuffer(char const *n)
      : name(n)
      , size(N)
      , buffer()
   {
      this->info();
      
      // Init first byte
      buffer[0] = 0xFF;
   }
   
   void info()
   {
      std::cout << "name : " << this->name << '\n';
      std::cout << "size : " << this->size << '\n';
   }
   
   void print()
   {
      for (uint32_t i = 0; i < this->size; ++i)
      {
         
         printf("0x%02X ", buffer[i]);
         
         // ...   
      }
   }
   
   // ...

private:
   std::string name;
   uint32_t size;
   uint8_t buffer[N];
   
   // ...
};

int main()
{
   constexpr char const *name = "TestBuffer";
   constexpr uint32_t size = 16;
    
   NamedFixedBuffer<size> nfb(name);
   nfb.print();
}

Uruchom w edytorze

name : TestBuffer                                                                                                                                                                           
size : 16                                                                                                                                                                                   
0xFF 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00

Problem w tym rozwiązaniu polega na tym, iż do inicjalizacji poszczególnych elementów tablicy zmuszeni jesteśmy do używania ciała konstruktora. Ponadto w naszym przypadku mamy dwa etapy inicjalizacji: pierwszy na liście inicjalizacyjnej (zerowanie wszystkich bajtów bufora), a drugi w ciele konstruktora (przypisywanie wartości do określonych bajtów – u nas dla uproszczenia jest to jeden, pierwszy bajt).

Zadanie to można wykonać w jednym kroku na liście inicjalizacyjnej, stosując rozszerzoną składnię opartą na nawiasach klamrowych, dostępną w ramach listy inicjalizacyjnej od standardu C++11:

#include <iostream>
#include <cstdio>

template<uint32_t N>
class NamedFixedBuffer
{
    static_assert(N > 0, "Buffer must have non-zero size");
public:
   NamedFixedBuffer(char const *n)
      : name(n)
      , size(N)
      , buffer{ 0xFF } // Init first byte
   {
      this->info();
   }
   
   void info()
   {
      std::cout << "name : " << this->name << '\n';
      std::cout << "size : " << this->size << '\n';
   }
   
   void print()
   {
      for (uint32_t i = 0; i < this->size; ++i)
      {
         
         printf("0x%02X ", buffer[i]);
         
         // ...   
      }
   }
   
   // ...

private:
   std::string name;
   uint32_t size;
   uint8_t buffer[N];
   
   // ...
};

int main()
{
   constexpr char const *name = "TestBuffer";
   constexpr uint32_t size = 16;
    
   NamedFixedBuffer<size> nfb(name);
   nfb.print();
}

Uruchom w edytorze

name : TestBuffer                                                                                                                                                                           
size : 16                                                                                                                                                                                   
0xFF 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00

Jak widać nie musimy specyfikować wszystkich wartości, bowiem pozostałe bedą zainicjalizowane wartościami domyślnymi (zerami). Dlatego też w naszej inicjalizacji wymieniliśmy tylko jeden (pierwszy) bajt, bowiem chcieliśmy aby pozostałe zostały zainicjalizowane zerami.

Konstruktory delegujące

Wróćmy teraz do przykładu klasy NamedBuffer, przy czym dodamy do niej konstruktor domyślny, inicjalizujący pola składowe z pewnymi domyślnymi ustawieniami (nazwa bufora i jego rozmiar):

#include <iostream>
#include <memory>

class NamedBuffer
{
public:
   // default ctor
   NamedBuffer()
      : name("Default")
      , size(256)
      , buffer(std::make_unique<uint8_t[]>(size))
   {
      this->info();
   }
   
   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 nb1(name, size);
   NamedBuffer nb2;
}

Uruchom w edytorze

name : TestBuffer                                                                                                                                                                           
size : 1024                                                                                                                                                                                 
name : Default                                                                                                                                                                              
size : 256

Łatwo zauważyć, że listy inicjalizacyjne w obu konstruktorach są bardzo podobne. Jedyna różnica polega na tym, iż konstruktor domyślny inicjalizuje pola składowe namesize ”na sztywno” pewnymi domyślnymi wartościami, natomiast drugi konstruktor  wykorzystuje w tym celu swoje parametry. Takie rozwiązanie stanie się uciążliwe, zwłaszcza kiedy kilka konstruktorów będzie musiało inicjalizować coraz większą ilość pól składowych. Wiąże się to z koniecznością utrzymywania podobnych list inicjalizacyjnych w kilku miejscach. Możemy oddelegować taką wspólną inicjalizację do innego konstruktora. W naszym przypadku proces inicjalizacji składowych oddelegujemy do konstruktora przyjmującego parametry, wywołując go na liście inicjalizacyjnej  konstruktora domyślnego, a który jest naszym konstruktorem delegującym (ang. delegating constructor):

#include <iostream>
#include <memory>

class NamedBuffer
{
public:
   // default ctor 
   NamedBuffer()
      : NamedBuffer("Defualt", 256) // delegation
   {
   }
   
   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 nb1(name, size);
   NamedBuffer nb2;
}

Uruchom w edytorze

name : TestBuffer                                                                                                                                                                           
size : 1024                                                                                                                                                                                 
name : Defualt                                                                                                                                                                              
size : 256

Konstruktory delegujące pojawiły się wraz ze standardem C++11, a ich niewątpliwą zaletą jest możliwość eliminacji duplikacji kodu z procesu konstrukcji obiektu. Dzięki temu nasz kod źródłowy staje się prostszy i łatwiejszy w utrzymaniu (ang. maintenance).

Inicjalizacja składowych w klasie

Pola składowe możemy także zainicjalizować bezpośrednio w klasie (jest to możliwe od standardu C++11). Inicjalizacja taka nastąpi, jeśli składowa nie zostanie jawnie wyspecyfikowana na liście inicjalizacyjnej. Takie podejście można więc zastosować do zdefiniowania domyślnych wartości pól składowych. Jako przykład ponownie niech posłużą nam pola składowe namesize:

#include <iostream>
#include <memory>

class NamedBuffer
{
public:
   // default ctor
   NamedBuffer()
      // no name and size members
      : buffer(std::make_unique<uint8_t[]>(size))
   {
      this->info();
   }
   
   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 = "Default"; // init (default)
   uint32_t size = 256;          // init (default)
   std::unique_ptr<uint8_t[]> buffer;
   
   // ...
};

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

Uruchom w edytorze

name : TestBuffer                                                                                                                                                                           
size : 1024                                                                                                                                                                                 
name : Default                                                                                                                                                                              
size : 256

Konstruktory dziedziczone

Załóżmy teraz, że chcemy rozszerzyć funkcjonalność naszego bufora o możliwość ładnego wyświetlania jego zawartości. W tym celu możemy zaimplementować nową klasę PrintableNamedBuffer dziedzicząc po klasie NamedBuffer:

#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;
   
   // ...
};

class PrintableNamedBuffer : public NamedBuffer
{
public:
   PrintableNamedBuffer(char const *n, uint32_t s)
      : NamedBuffer(n, s)
    {
    }
   
   // ...
};

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

Uruchom w edytorze

name : TestBuffer                                                                                                                                                                     
size : 1024

Jak widać konstruktor klasy PrintableNamedBuffer wywołuje analogiczny konstruktorklasy bazowej NamedBuffer (implementacja konstruktora z klasy NamedBuffer jest dla nas wystarczająca) przy czym oba konstruktory przyjmują te same parametry. Inaczej mówiąc, głównym zadaniem konstruktora klasy pochodnej jest przekazanie wszystkich swoich parametrów (z zachowaniem ich kolejności) do konstruktora klasy bazowej. W takiej sytuacji możemy skorzystać z cechy dostępnej w standardzie C++11 dotyczącej konstruktora dziedziczonego (ang. inherited constructor). Aby skorzystać z takiego rozwiązania, potrzebujemy przenieść symbol konstruktora klasy bazowej wraz z jego ewentualnymi przeciążeniami, za pomocą deklaracji ze słowem kluczowym using (ang. using declaration):

using NamedBuffer::NamedBuffer;

Przypomina to troche przenoszenie przesłoniętych symboli (ang. shadowing)  z zakresu klasy bazowej do zakresu widoczności klasy pochodnej (symbole z klasy bazowejprzesłonięte przez symbole z klasy pochodnej). W efekcie odziedziczony konstruktorklasy bazowej zachowuje się tak jak wcześniej jawny konstruktor klasy pochodnej, propagujący swoje parametry do konstruktora klasy bazowej (dodatkowo inicjalizując domyślnie swoje nietrywialnie składowe):

#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;
   
   // ...
};

class PrintableNamedBuffer : public NamedBuffer
{
public:
      using NamedBuffer::NamedBuffer; // "inherited" ctor
   
   // ...
};

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

Uruchom w edytorze

name : TestBuffer                                                                                                                                                                     
size : 1024

Dziedziczenie to nie dotyczy konstruktora domyślnego, kopiującegoprzenoszącego, bowiem  konstruktory takie zostaną wygenerowane przez kompilatorklasie pochodnej, także bez deklaracji ze słowem kluczowym using, Konstruktory te wywołują niejawnie swoje odpowiedniki z klasy bazowej. Zachowanie to jest znane jeszcze przed standardem C++11 – w odniesieniu do konstruktora domyślnegokonstruktora kopiującego (oczywiście przed standaredem C++11 nie istniało pojęcie konstruktora przenoszącego).

Funkcja std::exchange

Przyjmijmy teraz, że nasza implementacja bufora oparta jest na ”gołym” wskaźniku (w ogólności nie jest to zalecane podejście, bowiem zamiast tego powinniśmy stosować klasy oparte na idiomie RAII, do których należą klasy inteligentnych wskaźników). Ponadto chcemy mieć możliwość przenoszenia obiektu bufora. Skupimy się więc na implementacji przenoszącego konstruktora kopiującego (jednocześnie dla uproszczenia pomijając implementację przenoszącego operatora przypisania):

#include <iostream>
#include <memory>

class NamedBuffer
{
public:
   NamedBuffer(char const *n, uint32_t s)
      : name(n)
      , size(s)
      , buffer(new uint8_t[size])
   {
      this->info();
   }
   
   // move ctor
   NamedBuffer(NamedBuffer && other) noexcept
      : name(std::move(other.name))
      , size(other.size)
      , buffer(other.buffer)
   {
      other.size = 0;
      other.buffer = nullptr;
      
      this->info();
   }
   
   NamedBuffer & operator=(NamedBuffer && other) noexcept
   {
      // ...
      
      return *this;
   }
   
   ~NamedBuffer()
   {
      delete [] buffer;
      buffer = nullptr;
   }
   
   void info()
   {
      std::cout << "name   : " << this->name << '\n';
      std::cout << "size   : " << this->size << '\n';
      std::cout << "buffer : " << static_cast<void *>(this->buffer) << '\n';
      std::cout << '\n';
   }
   
   // ...

private:
   std::string name;
   uint32_t size;
   uint8_t *buffer; // raw pointer
   
   // ...
};

int main()
{
   constexpr char const *name = "TestBuffer";
   constexpr uint32_t size = 1024;
   
   NamedBuffer nb1(name, size);
   
   NamedBuffer nb2 = std::move(nb1);
   
   nb1.info();
}

Uruchom w edytorze

name   : TestBuffer                                                                                                                                                                     
size   : 1024                                                                                                                                                                           
buffer : 0x1f08c50                                                                                                                                                                      
                                                                                                                                                                                        
name   : TestBuffer                                                                                                                                                                     
size   : 1024                                                                                                                                                                           
buffer : 0x1f08c50                                                                                                                                                                      
                                                                                                                                                                                        
name   :                                                                                                                                                                                
size   : 0                                                                                                                                                                              
buffer : 0

konstruktorze przenoszącym przenosimy bezpośrednio, za pomocą funkcji std::move (plik nagłówkowy utility), składową name typu std::string (plik nagłówkowy string). Klasa std::string posiada swój własny konstruktor przenoszący, który w efekcie powoduje, że po przeniesieniu obiekt źródłowy tego typu przechowuje pusty łańcuch tekstowy. W przypadku typów prostych reprezentowanych przez składowe size (liczba całkowita) i buffer (wskaźnik) wykonujemy kopiowanie. Próba przeniesienia ich za pomocą funkcji std::move nie zmieni sytuacji i nadal będziemy mieć do czynienia z kopiowaniem. Spotkałem się z opinią, że jest to dobra praktyka. Zmodyfikujmy więc listę inicjalizacyjną w konstruktorze przenoszącym (teraz każda składowa podczas przenoszeni jest rzutowana na referencję do r-wartości za pośrednictwem funkcji std::move):

#include <iostream>
#include <utility>

class NamedBuffer
{
public:
   NamedBuffer(char const *n, uint32_t s)
      : name(n)
      , size(s)
      , buffer(new uint8_t[size])
   {
      this->info();
   }
   
   // move ctor
   NamedBuffer(NamedBuffer && other) noexcept
      : name(std::move(other.name))
      , size(std::move(other.size)) // unnecessary std::move but good practice
      , buffer(std::move(other.buffer)) // ditto
   {
      other.size = 0;
      other.buffer = nullptr;
      
      this->info();
   }
   
   NamedBuffer & operator=(NamedBuffer && other) noexcept
   {
      // ...
      
      return *this;
   }
   
   ~NamedBuffer()
   {
      delete [] buffer;
      buffer = nullptr;
   }
   
   void info()
   {
      std::cout << "name   : " << this->name << '\n';
      std::cout << "size   : " << this->size << '\n';
      std::cout << "buffer : " << static_cast<void *>(this->buffer) << '\n';
      std::cout << '\n';
   }
   
   // ...

private:
   std::string name;
   uint32_t size;
   uint8_t *buffer; // raw pointer
   
   // ...
};

int main()
{
   constexpr char const *name = "TestBuffer";
   constexpr uint32_t size = 1024;
   
   NamedBuffer nb1(name, size);
   
   NamedBuffer nb2 = std::move(nb1);
   
   nb1.info();
}

Uruchom w edytorze

name   : TestBuffer                                                                                                                                                                     
size   : 1024                                                                                                                                                                           
buffer : 0x19e3c50                                                                                                                                                                      
                                                                                                                                                                                        
name   : TestBuffer                                                                                                                                                                     
size   : 1024                                                                                                                                                                           
buffer : 0x19e3c50                                                                                                                                                                      
                                                                                                                                                                                        
name   :                                                                                                                                                                                
size   : 0                                                                                                                                                                              
buffer : 0

Przejdźmy teraz do ciała konstrukotra przenoszącego. Po przeniesieniu odpowiadających składowych naszym obowiązkiem jest pozostawienie obiektu źródłowego w prawidłowym stanie. W tym celu konieczne jest zerowanie wskaźnika buffer. W przecwinym razie, po wykonaniu operacji przeniesienia, otrzymamy dwa obiekty odwołujące się do tego samego obszaru pamięci. W efekcie oba obiekty w swoich destruktorach będą próbować zwalniać tę samą pamięć, co najprawdopodobniej doprowadzi do załamania programu. W przypadku składowej size, takie zerowanie nie jest konieczne, ale można je wykonać dla porządku. Inaczej mówiąc, obiekt źródłowy po przeniesieniu musi pozostawać w pewnym prawdłowym stanie, przy czym stan ten nie musi być z góry określony. Po przeniesieniu obiektu źródłowego zakładamy, że obiekt taki nie będzie później używany, przy czym po naszej stronie nadal pozostaje odpowiedzialność za jego prawidłowe niszczenie.

W tym momencie można zadać sobie pytanie, czy do przenoszenia obiektu musimy angażować ciało konstruktora przenoszącego. Okazuje się, że zadanie to można wykonać ograniczając się wyłącznie do listy inicjalizacyjnej tego konstruktora. Z pomocą przychodzi nam funkcja std::exchange (plik nagłówkowy utility) dostępna od standardu C++14. Oto przykładowa implementacja tej funkcji:

template<class T, class U = T>
T exchange(T& obj, U&& new_value)
{
    T old_value = std::move(obj);
    obj = std::forward<U>(new_value);
    return old_value;
}

Implementacja listy inicjalizacyjnej konstruktora przenoszącego, oparta na funkcji std::exchange, działa zgodnie z oczekiwaniami:

#include <iostream>
#include <utility>

class NamedBuffer
{
public:
   NamedBuffer(char const *n, uint32_t s)
      : name(n)
      , size(s)
      , buffer(new uint8_t[size])
   {
      this->info();
   }
   
   // move ctor
   NamedBuffer(NamedBuffer && other) noexcept
      : name(std::move(other.name))
      , size(std::exchange(other.size, 0))           // std::exchange
      , buffer(std::exchange(other.buffer, nullptr)) // ditto
   {
      // No assigment to members of source object
      
      this->info();
   }
   
   NamedBuffer & operator=(NamedBuffer && other) noexcept
   {
      // ...
      
      return *this;
   }
   
   ~NamedBuffer()
   {
      delete [] buffer;
      buffer = nullptr;
   }
   
   void info()
   {
      std::cout << "name   : " << this->name << '\n';
      std::cout << "size   : " << this->size << '\n';
      std::cout << "buffer : " << static_cast<void *>(this->buffer) << '\n';
      std::cout << '\n';
   }
   
   // ...

private:
   std::string name;
   uint32_t size;
   uint8_t *buffer; // raw pointer
   
   // ...
};

int main()
{
   constexpr char const *name = "TestBuffer";
   constexpr uint32_t size = 1024;
   
   NamedBuffer nb1(name, size);
   
   NamedBuffer nb2 = std::move(nb1);
   
   nb1.info();
}

Uruchom w edytorze

name   : TestBuffer                                                                                                                                                                     
size   : 1024                                                                                                                                                                           
buffer : 0x2375c50                                                                                                                                                                      
                                                                                                                                                                                        
name   : TestBuffer                                                                                                                                                                     
size   : 1024                                                                                                                                                                           
buffer : 0x2375c50                                                                                                                                                                      
                                                                                                                                                                                        
name   :                                                                                                                                                                                
size   : 0                                                                                                                                                                              
buffer : 0

Jak widać zadanie przeniesienia obiektu źródłowego można wykonać wykorzystując do tego wyłącznie listę inicjalizacyjną konstrukotra przenoszącego. Pozwala to uprościć implementację konstruktora przenoszącego, bowiem proces przenoszenia obiektu nie jest już rozdzielony na listę inicjalizacyjnąciało konstruktora. Inaczej mówiąc, takie rozwiązane nadal pozwala nam na tworzenie bardzo prostych konstruktorów, w których inicjalizacja odbywa się wyłącznie na liście inicjalizacyjnej (bez udziału ciała konstruktora).

Podsumowanie

  • Listy inicjalizacyjne umożliwiają inicjalizację wartości znajdujących się w tablicy (dzięki rozszerzonej składni z wykorzystaniem nawiasów klamrowych).
  • Konstruktor delegujący może wywołać na liście inicjalizacyjej inny konstruktor z tej samej klasy, delegując do niego zadanie konstrukcji obiektu – eliminuje to problem wynikający z konieczności implementacji podobnych list inicjalizacyjnych.
  • Pola składowe mogą być inicjalizowane wewnątrz klasy – jest inicjalizacja domyślna, która zachodzi gdy dane pole składowe nie jest inicjalizowane na liście inicjalizacyjnej wywoływanego konstruktora.
  • Dziedziczenie konstruktorów (z wyłączeniem konstruktora domyślnego, kopiującego oraz przenoszącego) jest możliwe dzięki deklaracji ze słowem kluczowym using. Dzięki temu w klasie pochodnej nie musimy implementować konstruktora, ktory na liście inicjalizacyjnej wywołuje wyłącznie konstruktorklasy bazowej, przekazując mu dokładnie te same parametry.
  • Funkcja std::exchange umożliwia prawidłowe przeniesienie obiektu wyłącznie z poziomu listy inicjalizacyjnej  (bez udziału ciała konstruktora).