Konstruktor z initializer_list

1

class Widget{
public:
Widget(initializer_list<double> list){} // #1
Widget(initializer_list<string> list){ } // #2
};
int main(){
Widget w{{}}; // Wywolany zostaje #1. Dlaczego #1 a nie #2?

return 0;

}

8

O, panie! Pytasz o chyba najbardziej tajemniczą część C++ ; > Od razu mówię, że nie podam pełnej odpowiedzi na twoje pytanie, bo jej nie znam. Zaznaczam też, że opisuję to jak to działa w C++14 (raz tylko wspomnę o C++17).

Całość sprowadza się do tego jaki typ ma brace initialization? Ano, tak de facto to nie ma typu ale w zależności od kontekstu może oznaczać coś co ma jakiś typ. Dla przykładu:

int x { 1 }; // int
auto x { 1 }; // std::initializer_list<int>
auto x { 1, 2, 3 }; // std::initializer_list<int>

Powstaje pytanie jaki typ zwróci auto, gdy użyjemy list initialization:

auto x {}; // błąd

Powyższy kod się nie skompiluje, ponieważ nie uda się wydedukować typu std::initializer_list<T>, bo nie znamy tego T. To się wydaje dość oczywiste.

Teraz zakładając, że mamy klasę, która posiada jeden konstruktor z std::initializer_list chcemy utworzyć obiekt tej klasy podając jako argument konstruktora empty list initialization:

struct foo{
    foo(std::initializer_list<int>) {}
};

//...
foo bar { {} };

Nie ma problemu, zawoła się powyższy konstruktor foo. Dlaczego to działa? Zgodnie z regułami list initialization, a dokładniej 8.5.4[2]:

A constructor is an initializer-list constructor if its first parameter is of type std::initializer_list<E> or
reference to possibly cv-qualified std::initializer_list<E> for some type E, and either there are no other
parameters or else all other parameters have default arguments (8.3.6). [ Note: Initializer-list constructors are
favored over other constructors in list-initialization (13.3.1.7).

Dalej 8.5.4[3]:

List-initialization of an object or reference of type T is defined as follows:
...
— Otherwise, if the initializer list has no elements, the object is value-initialized

Kolejno następuje value initialization, zatem za 8.5[8]:

To value-initialize an object of type T means:
...
— otherwise, the object is zero-initialized.

W tym miejscu kompilator potrafi już dopasować odpowiedni konstruktor.
Sprawa się komplikuje, gdy dodamy kolejny konstruktor, który przyjmuje std::initializer_list. Przykładowo:

struct foo{
    foo(std::initializer_list<int>) {}
    foo(std::initializer_list<float>) {}
};
//...
foo bar { {} }; // błąd dopasowania

Kompilator nie będzie wiedział w takim wypadku, który konstruktor powinien wybrać, bo oba pasują jednakowo. W takim wypadku trzeba explicit wskazać, który konstruktor chcemy wywołać:

foo bar { int{} }; // zawoła konstruktor z std::initializer_list<int>

Jeśli teraz zamienimy jeden z tych konstruktorów na taki, który przyjmuje std::initializer_list<std::string> i utworzymy obiekt tej klasy podając jako argument konstruktora empty list initialization to z jakiejś przyczyny (przypuszczam, że chodzi tutaj o bliższe dopasowanie zero initialization) overload resolution wybierze przeładowanie z std::initializer_list<int>:

struct foo{
    foo(std::initializer_list<int>) {}
    foo(std::initializer_list<std::string>) {}
};
//...
foo bar { {} }; // zawoła konstruktor z std::initializer_list<int>

Analogicznie możemy wymusić zawołanie drugiego konstruktora, np. w taki sposób:

    foo bar{std::string{}};
    foo bar{std::initializer_list<std::string>{}};

W tym temacie warto jeszcze poruszyć jedną rzecz. Specjalną regułę typowania dla zmiennej auto w kontekście uniform initialization. Przez to trzeba mówić o auto type deduction i template type dedection. Przykład:

auto x { 1, 2, 3 }; // auto type deduction więc x to initializer_list<int>
auto y = { 1, 2, 3 }; // jak wyżej
auto z { 1 }; // jak wyżej

Ponieważ tak naprawdę nikt nie rozumie dlaczego taka reguła jest w standardzie to wymyślony do niej poprawkę. Od C++1z (C++17) dla bezpośredniej, uniwersalnej inicjalizacji w użyciu z auto mamy nowe reguły typowania:

  • jeśli w nawiasach klamrowych jest tylko jeden element to mamy do czynienia z auto type deduction i wydedukowany typ będzie "odpowiadał" temu co podaliśmy w nawiasie
  • jeśli w nawiasach klamrowych będzie więcej niż jeden element to całe wyrażenie jest niepoprawne (spowoduje błąd kompilacji)
    Nowe reguły nie tykają range-for loop i copy-list-initialization. Przykład:
auto x = { 1, 2 }; // std::initializer_list<int>
auto x = { 1, 2.0 }; // błąd
auto x{ 1, 2 }; // błąd
auto x = { 3 }; // std::initializer_list<int>
auto x{ 3 }; // int

Link do proposala: http://www.open-std.org/jtc1/sc22/wg21/docs/papers/2014/n3922.html

Edit: W (niepełnej) odpowiedzi do pytania @pingwindyktator. Wydaje mi się, że overload list initialization z jakiejś przyczyny jest "mocniejszy".... Znalazłem w N2532 informację o tym, że wszystkie przeładowania dla std::initializer_list powinny być równoważne. W związku z tym imho powinno być tak, że jeśli konstruktorów z std::initializer_list jest więcej niż 1 i wołamy konstruktor z empty list initialization to zawsze powinniśmy otrzymywać błąd overloadu (bo można przypasować dowolny konstruktor).
Można za to obniżyć pozycję w overloadzie dowolnego z tych kontruktorów i wtedy overload zadziała bez problemu:

struct foo{
    foo(std::initializer_list<int>) {}
    
    template<typename = void>
    foo(std::initializer_list<std::string>) {}

    // albo przez explicit
    //explicit foo(std::initializer_list<std::string>) {}
}; 
//...
foo bar ( {} ); // wywoła konstruktor z std::initializer_list<int>
foo bar (std::initializer_list<std::string>{}); // wywoła konstruktor z std::initializer_list<std::string>
foo bar ( std::string{} ); // błąd
1

O patrzcie, tu działa po ludzku:
http://ideone.com/ZCUxyZ

1
1Czesław napisał(a):

Wywolany zostaje #1. Dlaczego #1 a nie #2?

A któż to wie. Bo tak. ;-) Nie wydaje mi się szczegółowa wiedza w tym temacie potrzebna, no może podczas idiotycznej rozmowy kwalifikacyjnej, ale to za parę lat jak C++11/14/17 będzie bardziej mainstreamowy.
Zwłaszcza jeśli reguła ma się zmienić jak @Satirev pisze - w praktyce będzie to oznaczało “compiler version specific” czyli de facto “implementation dependent”.

Satirev napisał(a)
foo bar { int{} }; // zawoła konstruktor z std::initializer_list<int>

Ale to wyśle jednego inta o wartości zero, jeśli ma być zero intów trzeba napisać

foo bar { initializer_list<int>{} }

EDIT:

auto x = { 3 }; // std::initializer_list<int>
auto x{ 3 }; // int

WUT? przez znak równości, który do tej pory był opcjonalny i nie miał znaczenia? „Jestem na NIE”.

2

Kod mi nie działa, więc pomyślałem, że może poczytam sobie dla rozrywki standard.... no i chyba zrozumiałem "co tu się wyprawia".

Generalnie wszystko jest wyjaśnione w 13.3.1.7 oraz 13.3.3.1.5[2] (w sumie to warto cały 13.3.3.1 przeczytać). W wielkim skrócie dla przypadku:

Widget w{{}};

mamy 1 elementową listę z empty list initializer, zatem rozważane są konstruktory z initializer list. Wewnętrzny {} sprowadza się do identity conversion - > double (wersja z std::string to już user defined conversion, która w overload resolution stoi niżej), koniec końców wybrana jest wersja z std::initializer_list<double>.

Z drugiej strony dla kodu:

Widget w({});

rozważane są wszystkie kostruktory (copy/move także, nawet te usunięte, bo jak wiemy usunięte metody także wchodzą w skład overload resolution) i nie udaje się wybrać najlepszego dopasowania.

Z podobnym problemem dopasowania będziemy mieli do czynienia, np. wtedy gdy więcej niż dla jednego konstruktora będzie mogło zajść identity conversion [13.3.3.2]:

struct foo{
    foo(std::initializer_list<double>){ cout << "1\n"; }
    foo(std::initializer_list<int>){ cout << "2\n"; }
};
foo {{}}; // nope

1 użytkowników online, w tym zalogowanych: 0, gości: 1