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;
}
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;
}
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:
auto type deduction
i wydedukowany typ będzie "odpowiadał" temu co podaliśmy w nawiasieauto 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
O patrzcie, tu działa po ludzku:
http://ideone.com/ZCUxyZ
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”.
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