Zgodnie z normą C++ (konkretnie [over.ics.list]), gdy zachodzi inicjalizacja listy, kompilator stara się dopasować listę z nawiasami klamrowymi do konstruktorów akceptujących std::initializer_list<T>. To dopasowanie stanowi konwersję tożsamościową (dokładne dopasowanie), która przeważa nad zdefiniowanymi przez użytkownika konwersjami wymaganymi do dopasowania pojedynczych elementów do konstruktorów inne niż initializer_list. W związku z tym konstruktor taki jak Container(size_t count, T value) przegrywa na rzecz Container(std::initializer_list<T>), gdy jest wywoływany z {10, 20}, ponieważ ten ostatni nie wymaga konwersji dla argumentu z nawiasami klamrowymi, niezależnie od zawężenia elementów.
Projektowaliśmy klasę Matrix dla silnika graficznego, która oferowała zarówno konstruktor Matrix(size_t rows, size_t cols, double val), jak i konstruktor stylu agregatu Matrix(std::initializer_list<std::initializer_list<double>>) do inicjalizacji literowych tabel. Młodszy programista napisał Matrix m{1080, 1920, 0.0}, oczekując macierzy 1080x1920 z inicjalizacją zerową, ale program stworzył macierz 1x3 zawierającą trzy wartości skalarne, co spowodowało subtelny błąd podczas renderowania, który był trudny do zdiagnozowania w trakcie sesji debugowania.
Początkowo rozważaliśmy wymuszenie składni w nawiasach Matrix(1080, 1920, 0.0) dla konstruktora wypełnienia, aby obejść przeciążenie std::initializer_list. Jednak naruszałoby to nasz standard kodowania, preferujący jednolitą inicjalizację w C++11 i tworzyło niespójną interfejs API, w którym niektóre konstruktory wymagały nawiasów, podczas gdy inne używały nawiasów klamrowych.
Następnie zbadaliśmy przekazywanie tagów, dodając parametr fill_tag_t do konstruktora wypełnienia, co skutecznie zmusiło użytkowników do pisania Matrix{fill_tag, 1080, 1920, 0.0}. Chociaż to rozjaśniło wywołanie, zagraciło publiczny interfejs i wprowadziło zamieszanie wśród programistów, którzy oczekiwali intuicyjnych sygnatur konstruktorów bez sztucznych typów tagów.
Po trzecie, próbowaliśmy ograniczyć konstruktor std::initializer_list do aktywacji tylko dla zagnieżdżonych nawiasów klamrowych za pomocą SFINAE na parametrze szablonu. To podejście złamało prawidłowe przypadki użycia, takie jak Matrix{{1.0, 2.0}, {3.0, 4.0}}, i wprowadziło kruchą metaprogramowanie szablonów, które zwiększyło czasy kompilacji i złożoność komunikatów o błędach.
Ostatecznie zdecydowaliśmy się wprowadzić statyczną funkcję fabryczną Matrix::filled(rows, cols, val) i uczyniliśmy prywatnym konstruktor wypełnienia z trzema parametrami, kierując użytkowników do jawnej składni dla konstrukcji opartych na wymiarach, zachowując jednocześnie konstruktor std::initializer_list jako publiczny dla składni agregacji. To zachowało intuicyjną inicjalizację w nawiasach klamrowych dla literowych tabel bez ryzyka przypadkowego błędnego zinterpretowania argumentów wymiarowych.
Zrefakrowany interfejs API uniemożliwił wystąpienie oryginalnego błędu, przekształcając Matrix{1080, 1920, 0.0} w błąd kompilacji bez dopasowanego publicznego konstruktora. Programiści byli teraz zmuszeni do korzystania z Matrix::filled(1080, 1920, 0.0) w celu operacji wypełnienia lub Matrix{{...}} dla list inicjalizacyjnych, co znacznie poprawiło przejrzystość kodu i bezpieczeństwo.
Jak kompilator ocenia sekwencję konwersji z listy inicjalizacyjnej w nawiasach klamrowych do konstruktora, który nie jest initializer_list, w porównaniu do dopasowania tożsamości konstruktor?
Zgodnie z zasadami rozwiązywania przeciążeń normy C++ dla inicjalizacji listy, wiązanie listy z nawiasami klamrowymi do parametru std::initializer_list<T> stanowi konwersję tożsamościową (dokładne dopasowanie) z najwyższym rankingiem. W przeciwieństwie do tego dopasowanie tej samej listy z nawiasami klamrowymi do innego konstruktora wymaga, aby kompilator potraktował listę jako listę wyrażenia w nawiasach i przeprowadził zdefiniowane przez użytkownika lub standardowe konwersje dla każdego elementu. Ponieważ konwersje tożsamościowe przewyższają wszystkie inne sekwencje konwersji, konstruktor initializer_list wygrywa, nawet jeśli jego typy elementów są gorszym dopasowaniem logicznym niż te wymagane przez alternatywnego konstruktora.
Dlaczego auto x = {1, 2, 3}; dedukuje std::initializer_list<int> w C++11 i C++14, podczas gdy auto x{1, 2, 3} staje się nieważne w C++17 i później?
Przed C++17 inicjalizacja listy kopiowania przy użyciu tokena = z auto zawsze dedukowała std::initializer_list dla list z nawiasami klamrowymi. Jednak C++17 wprowadził nowe zasady dotyczące bezpośredniej inicjalizacji listy z auto (bez =), które przeprowadzają standardową dedukcję argumentów szablonu: jeśli lista z nawiasami klamrowymi zawiera wiele elementów, dedukcja nie udaje się, ponieważ auto nie może reprezentować std::initializer_list w tym kontekście, co sprawia, że program staje się nieważny. Ta zmiana eliminuje pułapkę „ukrytego std::initializer_list” dla bezpośredniej inicjalizacji, jednak kandydaci często przeoczają, że składnia kopiowania (auto x = {...}) nadal dedukuje std::initializer_list nawet w nowoczesnym C++, tworząc subtelną niespójność między stylami inicjalizacji.
W jakim scenariuszu klasa z konstruktorem initializer_list oraz konstruktorem szablonowym o zmiennej liczbie argumentów może rozwiązać problem niejednoznaczności i jak std::in_place_t może rozwiązać ten problem?
Gdy klasa oferuje zarówno Container(std::initializer_list<T>), jak i template<typename... Args> Container(Args&&... args), pakiet zmiennych argumentów może dopasować te same argumenty, co konstruktor initializer_list poprzez dedukcję argumentów szablonu. Dla Container c{1, 2, 3}, oba konstruktory są wykonalne: pierwszy poprzez konwersję tożsamościową listy z nawiasami klamrowymi, a drugi poprzez dedukcję Args jako int, int, int. Chociaż konstruktor initializer_list niebędący szablonem zwykle wygrywa przy rozstrzyganie w przypadku remisu, dodanie typu tagu, takiego jak std::in_place_t do konstruktora szablonowego (np. Container(std::in_place_t, Args&&... args)) zmusza użytkowników do pisania Container{std::in_place, 1, 2, 3}, zapewniając, że wersja szablonowa jest wywoływana tylko jawnie, podczas gdy konstruktor initializer_list obsługuje jednorodne listy z nawiasami klamrowymi domyślnie.