23.7 — std::initializer_list

Rozważ stałą tablicę liczb całkowitych w C++:

int array[5];

Jeśli chcemy zainicjować tę tablicę wartościami, możemy to zrobić bezpośrednio za pomocą składni listy inicjatorów:

#include <iostream>

int main()
{
	int array[] { 5, 4, 3, 2, 1 }; // initializer list
	for (auto i : array)
		std::cout << i << ' ';

	return 0;
}

Wypisuje:

5 4 3 2 1

To działa również w przypadku tablic alokowanych dynamicznie:

#include <iostream>

int main()
{
	auto* array{ new int[5]{ 5, 4, 3, 2, 1 } }; // initializer list
	for (int count{ 0 }; count < 5; ++count)
		std::cout << array[count] << ' ';
	delete[] array;

	return 0;
}

W poprzedniej lekcji przedstawiliśmy koncepcję klas kontenerów i pokazaliśmy przykład klasy IntArray przechowującej tablicę liczb całkowitych:

#include <cassert> // for assert()
#include <iostream>
 
class IntArray
{
private:
    int m_length{};
    int* m_data{};
 
public:
    IntArray() = default;
 
    IntArray(int length)
        : m_length{ length }
	, m_data{ new int[static_cast<std::size_t>(length)] {} }
    {
    }
 
    ~IntArray()
    {
        delete[] m_data;
        // we don't need to set m_data to null or m_length to 0 here, since the object will be destroyed immediately after this function anyway
    }
 
    int& operator[](int index)
    {
        assert(index >= 0 && index < m_length);
        return m_data[index];
    }
 
    int getLength() const { return m_length; }
};

int main()
{
	// What happens if we try to use an initializer list with this container class?
	IntArray array { 5, 4, 3, 2, 1 }; // this line doesn't compile
	for (int count{ 0 }; count < 5; ++count)
		std::cout << array[count] << ' ';

	return 0;
}

Ten kod nie zostanie skompilowany, ponieważ klasa IntArray nie ma konstruktora, który wie, co zrobić z listą inicjatorów. W rezultacie pozostaje nam inicjowanie naszych elementów tablicy indywidualnie:

int main()
{
	IntArray array(5);
	array[0] = 5;
	array[1] = 4;
	array[2] = 3;
	array[3] = 2;
	array[4] = 1;

	for (int count{ 0 }; count < 5; ++count)
		std::cout << array[count] << ' ';

	return 0;
}

To nie jest takie wspaniałe.

Inicjalizacja klasy przy użyciu std::initializer_list

Gdy kompilator widzi listę inicjatorów, automatycznie konwertuje ją na obiekt typu std::initializer_list. Dlatego jeśli utworzymy konstruktor, który przyjmuje parametr std::initializer_list, możemy tworzyć obiekty, używając listy inicjatorów jako danych wejściowych.

std::initializer_list żyje w <initializer_list> .

Jest kilka rzeczy, które warto wiedzieć o std::initializer_list. Podobnie jak std::array lub std::vector, musisz powiedzieć std::initializer_list, jaki typ danych zawiera lista, używając nawiasów ostrych, chyba że od razu zainicjujesz std::initializer_list. Dlatego prawie nigdy nie zobaczysz zwykłej std::initializer_list. Zamiast tego zobaczysz coś w stylu std::initializer_list<int> lub std::initializer_list<std::string>.

Po drugie, std::initializer_list zawiera (błędnie nazwaną) funkcję size(), która zwraca liczbę elementów na liście. Jest to przydatne, gdy musimy znać długość przekazywanej listy.

Po trzecie, std::initializer_list jest często przekazywana przez wartość. Podobnie jak std::string_view, std::initializer_list jest widokiem. Kopiowanie std::initializer_list nie powoduje skopiowania elementów listy.

Przyjrzyjmy się aktualizacji naszej klasy IntArray za pomocą konstruktora, który pobiera std::initializer_list.

#include <algorithm> // for std::copy
#include <cassert> // for assert()
#include <initializer_list> // for std::initializer_list
#include <iostream>

class IntArray
{
private:
	int m_length {};
	int* m_data{};

public:
	IntArray() = default;

	IntArray(int length)
		: m_length{ length }
		, m_data{ new int[static_cast<std::size_t>(length)] {} }
	{

	}

	IntArray(std::initializer_list<int> list) // allow IntArray to be initialized via list initialization
		: IntArray(static_cast<int>(list.size())) // use delegating constructor to set up initial array
	{
		// Now initialize our array from the list
		std::copy(list.begin(), list.end(), m_data);
	}

	~IntArray()
	{
		delete[] m_data;
		// we don't need to set m_data to null or m_length to 0 here, since the object will be destroyed immediately after this function anyway
	}

	IntArray(const IntArray&) = delete; // to avoid shallow copies
	IntArray& operator=(const IntArray& list) = delete; // to avoid shallow copies

	int& operator[](int index)
	{
		assert(index >= 0 && index < m_length);
		return m_data[index];
	}

	int getLength() const { return m_length; }
};

int main()
{
	IntArray array{ 5, 4, 3, 2, 1 }; // initializer list
	for (int count{ 0 }; count < array.getLength(); ++count)
		std::cout << array[count] << ' ';

	return 0;
}

Daje to oczekiwany wynik:

5 4 3 2 1

To działa! Przyjrzyjmy się teraz temu bardziej szczegółowo.

Oto nasz konstruktor IntArray, który pobiera std::initializer_list<int>.

	IntArray(std::initializer_list<int> list) // allow IntArray to be initialized via list initialization
		: IntArray(static_cast<int>(list.size())) // use delegating constructor to set up initial array
	{
		// Now initialize our array from the list
		std::copy(list.begin(), list.end(), m_data);
	}

w linii 1: Jak wspomniano powyżej, musimy użyć nawiasów kątowych, aby określić, jakiego typu elementu oczekujemy na liście. W tym przypadku, ponieważ jest to tablica IntArray, spodziewalibyśmy się, że lista będzie wypełniona wartością typu int. Pamiętaj, że nie przekazujemy listy poprzez odwołanie do stałej. Podobnie jak std::string_view, std::initializer_list jest bardzo lekki, a kopie są zwykle tańsze niż pośrednie.

W linii 2: Delegujemy przydzielanie pamięci dla IntArray innemu konstruktorowi za pośrednictwem konstruktora delegującego (aby zredukować nadmiarowy kod). Ten inny konstruktor musi znać długość tablicy, dlatego przekazujemy mu list.size(), która zawiera liczbę elementów na liście. Zauważ, że list.size() zwraca size_t (bez znaku), więc musimy tutaj rzutować na int ze znakiem.

Treść konstruktora jest zarezerwowana do kopiowania elementów z listy do naszej klasy IntArray. Najłatwiej to zrobić, używając std::copy(), który znajduje się w nagłówku <algorithm>.

Dostęp do elementów std::initializer_list

W niektórych przypadkach możesz chcieć uzyskać dostęp do każdego elementu std:initializer_list przed skopiowaniem tego elementu do tablicy wewnętrznej (np. w celu sprawdzenia poprawności wartości lub zmodyfikowania tych wartości jakoś).

Z jakiegoś niewytłumaczalnego powodu std::initializer_list nie zapewnia dostępu do elementów listy poprzez indeks dolny (operator[]). Komitetowi normalizacyjnym wielokrotnie zwracano uwagę na to pominięcie i nigdy się nim nie zajęto.

Istnieje jednak wiele łatwych obejść:

  1. Możesz użyć pętli for opartej na zakresach, aby iterować po elementach listy.
  2. Innym sposobem jest użycie begin() funkcji składowej w celu uzyskania iteratora do std::initializer_list. Ponieważ ten iterator jest iteratorem o dostępie swobodnym, iteratory można indeksować:
	IntArray(std::initializer_list<int> list) // allow IntArray to be initialized via list initialization
		: IntArray(static_cast<int>(list.size())) // use delegating constructor to set up initial array
	{
		// Now initialize our array from the list
		for (std::size_t count{}; count < list.size(); ++count)
		{
			m_data[count] = list.begin()[count];
		}
	}

Inicjowanie listy preferuje konstruktory listowe zamiast konstruktorów nielistowych.

Niepuste listy inicjatorów zawsze będą faworyzować pasujący konstruktor inicjalizator_list w stosunku do innych potencjalnie pasujących konstruktorów. Rozważmy:

IntArray a1(5);   // uses IntArray(int), allocates an array of size 5
IntArray a2{ 5 }; // uses IntArray<std::initializer_list<int>, allocates array of size 1

Klasa a1 case używa bezpośredniej inicjalizacji (która nie uwzględnia konstruktorów list), więc ta definicja wywoła IntArray(int), alokując tablicę o rozmiarze 5.

Klasa a2 case używa inicjalizacji listy (co faworyzuje konstruktory list). Obydwa IntArray(int) i IntArray(std::initializer_list<int>) są tutaj możliwymi dopasowaniami, ale ponieważ preferowane są konstruktory list, zostanie wywołany IntArray(std::initializer_list<int>) , przydzielający tablicę o rozmiarze 1 (z tym elementem o wartości 5)

Dlatego nasz konstruktor delegujący powyżej używa bezpośredniej inicjalizacji podczas delegowania:

	IntArray(std::initializer_list<int> list)
		: IntArray(static_cast<int>(list.size())) // uses direct init

To gwarantuje, że delegujemy do wersji IntArray(int) . Gdybyśmy zamiast tego delegowali przy użyciu inicjalizacji listy, konstruktor próbowałby delegować do siebie, co spowodowałoby błąd kompilacji.

To samo dzieje się z std::vector i innymi klasami kontenerów, które mają zarówno konstruktor listowy, jak i konstruktor z parametrem podobnego typu

std::vector<int> array(5); // Calls std::vector::vector(std::vector::size_type), 5 value-initialized elements: 0 0 0 0 0
std::vector<int> array{ 5 }; // Calls std::vector::vector(std::initializer_list<int>), 1 element: 5

Kluczowa informacja

Inicjalizacja listy faworyzuje pasujące konstruktory listowe zamiast pasujących konstruktorów nielistowych.

Najlepsza praktyka

Podczas inicjowania kontenera zawierającego listę konstruktor:

  • Użyj inicjalizacji nawiasów klamrowych, gdy zamierzasz wywołać konstruktor listy (np. ponieważ inicjatory są wartościami elementów)
  • Użyj bezpośredniej inicjalizacji, gdy zamierzasz wywołać konstruktor niebędący listą (np. ponieważ inicjalizatory nie są wartościami elementów).

Dodawanie konstruktorów list do istniejącej klasy jest niebezpieczne

Ponieważ inicjalizacja listy faworyzuje konstruktory list, dodanie konstruktora listy do istniejącej klasy, która wcześniej go nie posiadała, może spowodować cichą zmianę zachowania istniejących programów.

Rozważ następujący program:

#include <initializer_list> // for std::initializer_list
#include <iostream>

class Foo
{
public:
	Foo(int, int)
	{
		std::cout << "Foo(int, int)" << '\n';
	}
};

int main()
{
	Foo f1{ 1, 2 }; // calls Foo(int, int)

	return 0;
}

Wypisuje:

Foo(int, int)

Dodajmy teraz konstruktor listy do tej klasy:

#include <initializer_list> // for std::initializer_list
#include <iostream>

class Foo
{
public:
	Foo(int, int)
	{
		std::cout << "Foo(int, int)" << '\n';
	}

	// We've added a list constructor
	Foo(std::initializer_list<int>)
	{
		std::cout << "Foo(std::initializer_list<int>)" << '\n';
	}

};

int main()
{
	// note that the following statement has not changed
	Foo f1{ 1, 2 }; // now calls Foo(std::initializer_list<int>)

	return 0;
}

Chociaż nie dokonaliśmy żadnych innych zmian w programie, ten program teraz wypisuje:

Foo(std::initializer_list<int>)

Ostrzeżenie

Dodanie konstruktora listy do istniejącej klasy, która go nie posiadała, może przerwać istniejącą programy.

Przypisanie klasy za pomocą std::initializer_list

Możesz także użyć std::initializer_list, aby przypisać nowe wartości do klasy poprzez przeciążenie operatora przypisania w celu pobrania parametru std::initializer_list. Działa to analogicznie do powyższego. Poniżej pokażemy przykład, jak to zrobić w rozwiązaniu quizu.

Pamiętaj, że jeśli zaimplementujesz konstruktor korzystający z std::initializer_list, powinieneś upewnić się, że wykonałeś co najmniej jedną z następujących czynności:

  1. Podaj operator przypisania przeciążonej listy
  2. Podaj poprawny operator przypisania kopiowania do głębokiego kopiowania
  3. Usuń przypisanie kopii operator

Oto dlaczego: rozważ następującą klasę (która nie ma żadnej z tych rzeczy) wraz z instrukcją przypisania listy:

#include <algorithm> // for std::copy()
#include <cassert>   // for assert()
#include <initializer_list> // for std::initializer_list
#include <iostream>

class IntArray
{
private:
	int m_length{};
	int* m_data{};

public:
	IntArray() = default;

	IntArray(int length)
		: m_length{ length }
		, m_data{ new int[static_cast<std::size_t>(length)] {} }
	{

	}

	IntArray(std::initializer_list<int> list) // allow IntArray to be initialized via list initialization
		: IntArray(static_cast<int>(list.size())) // use delegating constructor to set up initial array
	{
		// Now initialize our array from the list
		std::copy(list.begin(), list.end(), m_data);
	}

	~IntArray()
	{
		delete[] m_data;
	}

//	IntArray(const IntArray&) = delete; // to avoid shallow copies
//	IntArray& operator=(const IntArray& list) = delete; // to avoid shallow copies

	int& operator[](int index)
	{
		assert(index >= 0 && index < m_length);
		return m_data[index];
	}

	int getLength() const { return m_length; }
};

int main()
{
	IntArray array{};
	array = { 1, 3, 5, 7, 9, 11 }; // Here's our list assignment statement

	for (int count{ 0 }; count < array.getLength(); ++count)
		std::cout << array[count] << ' '; // undefined behavior

	return 0;
}

Po pierwsze, kompilator zauważy, że funkcja przypisania przyjmująca std::initializer_list nie istnieje. Następnie będzie szukać innych funkcji przypisania, których mógłby użyć, i odkryje podany niejawnie operator przypisania kopiowania. Jednak tej funkcji można użyć tylko wtedy, gdy można przekonwertować listę inicjatorów na tablicę IntArray. Ponieważ { 1, 3, 5, 7, 9, 11 } jest listą std::initializer_list, kompilator użyje konstruktora listy do przekonwertowania listy inicjatorów na tymczasową tablicę IntArray. Następnie wywoła ukryty operator przypisania, który płytko skopiuje tymczasowy IntArray do naszego obiektu tablicowego.

W tym momencie zarówno m_data tymczasowej IntArray, jak i array->m_data wskazują na ten sam adres (ze względu na płytką kopię). Już widać, dokąd to zmierza.

Na końcu instrukcji przypisania tymczasowa tablica IntArray zostaje zniszczona. To wywołuje destruktor, który usuwa m_data tymczasowej tablicy IntArray. Pozostawia to array->m_data jako wiszący wskaźnik. Kiedy spróbujesz użyć array->m_data w jakimkolwiek celu (na przykład wtedy, gdy tablica wyjdzie poza zakres, a destruktor usunie m_data), otrzymasz niezdefiniowane zachowanie.

Najlepsza praktyka

Jeśli podasz konstrukcję listy, dobrym pomysłem jest zapewnienie również przypisania listy.

Streszczenie

Implementacja konstruktora pobierającego parametr std::initializer_list pozwala nam używać inicjalizacji listy z naszymi niestandardowymi klasami. Możemy również użyć std::initializer_list do zaimplementowania innych funkcji, które muszą używać listy inicjatorów, takich jak operator przypisania.

Czas quizu

Pytanie nr 1

Korzystając z powyższej klasy IntArray, zaimplementuj przeciążony operator przypisania, który pobiera listę inicjatorów.

Należy uruchomić następujący kod:

int main()
{
	IntArray array { 5, 4, 3, 2, 1 }; // initializer list
	for (int count{ 0 }; count < array.getLength(); ++count)
		std::cout << array[count] << ' ';

	std::cout << '\n';

	array = { 1, 3, 5, 7, 9, 11 };

	for (int count{ 0 }; count < array.getLength(); ++count)
		std::cout << array[count] << ' ';

	std::cout << '\n';

	return 0;
}

To powinno zostać wydrukowane:

5 4 3 2 1 
1 3 5 7 9 11

Pokaż rozwiązanie

guest
Twój adres e-mail nie zostanie wyświetlony
Znalazłeś błąd? Zostaw komentarz powyżej!
Komentarze związane z poprawkami zostaną usunięte po przetworzeniu, aby pomóc zmniejszyć bałagan. Dziękujemy za pomoc w ulepszaniu witryny dla wszystkich!
Awatary z https://gravatar.com/ są połączone z podanym adresem e-mail.
Powiadamiaj mnie o odpowiedziach:  
296 Komentarze
Najnowsze
Najstarsze Najczęściej głosowane
Wbudowane opinie
Wyświetl wszystkie komentarze