Dlaczego inicjalizacja listy (przy użyciu nawiasów klamrowych) jest lepsza niż alternatywy?

405
MyClass a1 {a};     // clearer and less error-prone than the other three
MyClass a2 = {a};
MyClass a3 = a;
MyClass a4(a);

Dlaczego?

Nie mogłem znaleźć odpowiedzi na SO, więc pozwól mi odpowiedzieć na własne pytanie.

Ołeksij
źródło
12
Dlaczego nie użyć auto?
Mark Garcia
33
To prawda, jest to wygodne, ale moim zdaniem zmniejsza czytelność - lubię widzieć, jaki typ obiektu jest podczas czytania kodu. Jeśli jesteś w 100% pewien, jakiego typu jest ten obiekt, po co korzystać z auto? A jeśli użyjesz inicjalizacji listy (przeczytaj moją odpowiedź), możesz być pewien, że zawsze jest poprawna.
Oleksiy
102
@Oleksiy: std::map<std::string, std::vector<std::string>>::const_iteratorchciałbym z tobą porozmawiać .
Xeo
9
@Oleksiy Polecam przeczytanie tego GotW .
Rapptz
17
@doc Powiedziałbym, że using MyContainer = std::map<std::string, std::vector<std::string>>;jest jeszcze lepszy (zwłaszcza, że ​​można to szablon!)
JAB

Odpowiedzi:

356

Zasadniczo kopiowanie i wklejanie z „The C ++ Programming Language 4th Edition” Bjarne Stroustrupa :

Inicjalizacja listy nie pozwala na zawężenie (§iso.8.5.4). To jest:

  • Liczby całkowitej nie można przekonwertować na inną liczbę całkowitą, która nie może utrzymać swojej wartości. Na przykład char do int jest dozwolone, ale nie int do char.
  • Wartości zmiennoprzecinkowej nie można przekonwertować na inny typ zmiennoprzecinkowy, który nie może utrzymać swojej wartości. Na przykład dozwolona jest liczba zmiennoprzecinkowa, ale nie liczba zmiennoprzecinkowa
  • Wartości zmiennoprzecinkowej nie można przekonwertować na typ całkowity.
  • Wartości całkowitej nie można przekonwertować na typ zmiennoprzecinkowy.

Przykład:

void fun(double val, int val2) {

    int x2 = val; // if val==7.9, x2 becomes 7 (bad)

    char c2 = val2; // if val2==1025, c2 becomes 1 (bad)

    int x3 {val}; // error: possible truncation (good)

    char c3 {val2}; // error: possible narrowing (good)

    char c4 {24}; // OK: 24 can be represented exactly as a char (good)

    char c5 {264}; // error (assuming 8-bit chars): 264 cannot be 
                   // represented as a char (good)

    int x4 {2.0}; // error: no double to int value conversion (good)

}

Jedynie sytuacja, w której jest korzystniejszy = {} jest przy użyciu autosłowa kluczowego, aby uzyskać typ określony przez inicjatora.

Przykład:

auto z1 {99};   // z1 is an int
auto z2 = {99}; // z2 is std::initializer_list<int>
auto z3 = 99;   // z3 is an int

Wniosek

Preferuj inicjalizację {} zamiast alternatywnych, chyba że masz wyraźny powód, aby tego nie robić.

Ołeksij
źródło
52
Istnieje również fakt, że użycie ()może zostać przeanalizowane jako deklaracja funkcji. To mylące i niespójne, że można powiedzieć, T t(x,y,z);ale nie T t(). A czasami, na pewno x, nie możesz nawet powiedzieć T t(x);.
juanchopanza
84
Zdecydowanie nie zgadzam się z tą odpowiedzią; inicjalizacja wzmocniona staje się kompletnym bałaganem, gdy masz typy z ctor akceptującym std::initializer_list. RedXIII wspomina o tym problemie (i po prostu go usuwa), a ty go całkowicie ignorujesz. A(5,4)i A{5,4}może wywoływać zupełnie różne funkcje, i to jest ważna rzecz, o której należy wiedzieć. Może to nawet prowadzić do połączeń, które wydają się nieintuicyjne. Powiedzenie, że powinieneś preferować {}, doprowadzi do nieporozumień. Ale to nie twoja wina. Osobiście uważam, że jest to wyjątkowo źle przemyślana funkcja.
user1520427
13
@ user1520427 Właśnie dlatego istnieje część „ chyba, że ​​masz silny powód, aby się nie ”.
Oleksiy
67
Chociaż to pytanie jest stare, ma sporo trafień, dlatego dodam je tutaj tylko w celach informacyjnych (nigdzie indziej na stronie nie widziałem). Od wersji C ++ 14 z nowymi regułami automatycznego odliczania od spisu-list-inicjacji można teraz pisać auto var{ 5 }i będzie on wywnioskowany jakoint nie więcej niż std::initializer_list<int>.
Edoardo Sparkon Dominici
12
Haha, ze wszystkich komentarzy wciąż nie jest jasne, co robić. Oczywiste jest, że specyfikacja C ++ to bałagan!
DrumM
113

Istnieją już świetne odpowiedzi na temat zalet korzystania z inicjalizacji listy, jednak moją osobistą zasadą jest, aby NIE używać nawiasów klamrowych, gdy tylko jest to możliwe, ale uzależnić je od znaczenia pojęciowego:

  • Jeśli obiekt, który tworzę koncepcyjnie, zawiera wartości, które przekazuję w konstruktorze (np. Kontenery, struktury POD, atomics, inteligentne wskaźniki itp.), To używam nawiasów klamrowych.
  • Jeśli konstruktor przypomina normalne wywołanie funkcji (wykonuje mniej lub bardziej złożone operacje sparametryzowane przez argumenty), to używam normalnej składni wywołania funkcji.
  • Do domyślnej inicjalizacji zawsze używam nawiasów klamrowych.
    Po pierwsze, w ten sposób zawsze jestem pewien, że obiekt zostanie zainicjowany niezależnie od tego, czy np. Jest to „prawdziwa” klasa z domyślnym konstruktorem, który i tak zostanie wywołany, czy też ma wbudowany / POD typ. Po drugie - w większości przypadków - jest zgodny z pierwszą regułą, ponieważ domyślnie zainicjowany obiekt często reprezentuje „pusty” obiekt.

Z mojego doświadczenia wynika, że ​​ten zestaw reguł można stosować znacznie bardziej konsekwentnie niż domyślnie stosowanie nawiasów klamrowych, ale trzeba wyraźnie pamiętać wszystkie wyjątki, gdy nie można ich użyć lub mają inne znaczenie niż „normalna” składnia wywołania funkcji z nawiasami (wywołuje inne przeciążenie).

Np. Dobrze pasuje do standardowych typów bibliotek, takich jak std::vector:

vector<int> a{10,20};   //Curly braces -> fills the vector with the arguments

vector<int> b(10,20);   //Parentheses -> uses arguments to parametrize some functionality,                          
vector<int> c(it1,it2); //like filling the vector with 10 integers or copying a range.

vector<int> d{};      //empty braces -> default constructs vector, which is equivalent
                      //to a vector that is filled with zero elements
MikeMB
źródło
11
Całkowicie zgadzam się z większością odpowiedzi. Nie sądzisz jednak, że umieszczanie pustych nawiasów klamrowych dla wektora jest po prostu zbędne? Mam na myśli, że jest ok, kiedy trzeba zainicjować wartość obiektu typu ogólnego T, ale jaki jest cel tego w przypadku kodu nieogólnego?
Michaił
8
@Mikhail: Z pewnością jest zbędny, ale moim zwyczajem jest zawsze jawne inicjowanie zmiennych lokalnych. Jak pisałem, chodzi głównie o sumienie, więc nie zapominam o tym, kiedy ma to znaczenie. Z pewnością nie wspomnę o tym w recenzji kodu ani w przewodniku po stylu.
MikeMB
4
całkiem czysty zestaw reguł.
laike9m
5
To zdecydowanie najlepsza odpowiedź. {} jest jak dziedziczenie - łatwe do nadużyć, co prowadzi do trudnego do zrozumienia kodu.
UKMonkey
2
@MikeMB przykład: const int &b{}<- nie próbuje utworzyć niezainicjowanego odwołania, ale wiąże je z tymczasowym obiektem całkowitoliczbowym. Drugi przykład: struct A { const int &b; A():b{} {} };<- nie próbuje utworzyć niezainicjowanego odwołania (jak ()by to zrobił), ale wiąże go z tymczasowym obiektem całkowitym, a następnie pozostawia wisi. Nawet GCC -Wallnie ostrzega o drugim przykładzie.
Johannes Schaub - litb
91

Istnieje wiele powodów, aby inicjalizacji użycie nawiasów, ale należy pamiętać, że konstruktor jest korzystne dla innych konstruktorów , wyjątkiem jest domyślnym-konstruktor. Prowadzi to do problemów z konstruktorami i szablonami, w których konstruktorem typów może być lista inicjalizująca lub zwykły stary ctor.initializer_list<>T

struct Foo {
    Foo() {}

    Foo(std::initializer_list<Foo>) {
        std::cout << "initializer list" << std::endl;
    }

    Foo(const Foo&) {
        std::cout << "copy ctor" << std::endl;
    }
};

int main() {
    Foo a;
    Foo b(a); // copy ctor
    Foo c{a}; // copy ctor (init. list element) + initializer list!!!
}

Zakładając, że nie spotkasz się z takimi klasami, nie ma powodu, aby nie używać listy intializatora.

Czerwony XIII
źródło
20
Jest to bardzo ważny punkt w programowaniu ogólnym. Kiedy piszesz szablony, nie używaj braced-init-list (nazwa standardu dla { ... }), chyba że chcesz initializer_listsemantyki (cóż, a być może do domyślnego konstruowania obiektu).
Xeo
82
Naprawdę nie rozumiem, dlaczego ta std::initializer_listreguła w ogóle istnieje - to tylko wprowadza zamieszanie i bałagan w języku. Co jest złego w robieniu, Foo{{a}}jeśli chcesz std::initializer_listkonstruktora? Wydaje się to o wiele łatwiejsze do zrozumienia niż std::initializer_listmieć pierwszeństwo przed wszystkimi innymi przeciążeniami.
user1520427
5
+1 za powyższy komentarz, ponieważ myślę, że to naprawdę bałagan !! to nie jest logika; Foo{{a}}kieruje się dla mnie logiką znacznie bardziej niż ta, Foo{a}która zmienia się w pierwszeństwo listy inicjalizatorów (ups może pomyśleć hm ...)
Gabriel
32
Zasadniczo C ++ 11 zastępuje jeden bałagan innym. Och, przepraszam, że to nie zastępuje - dodaje. Skąd możesz wiedzieć, że nie spotkasz się z takimi klasami? Co jeśli zaczniesz bez std::initializer_list<Foo> konstruktora, ale w pewnym momencie zostanie on dodany do Fooklasy w celu rozszerzenia interfejsu? Następnie użytkownicy Fooklasy są popieprzeni.
dok
10
.. jakie są „WIELE POWODÓW użycia inicjalizacji nawiasu klamrowego”? Ta odpowiedź wskazuje jeden powód ( initializer_list<>), który tak naprawdę nie kwalifikuje, kto twierdzi, że jest preferowany, a następnie wspomina o jednym dobrym przypadku, w którym NIE jest preferowany. Czego mi brakuje, że ok. 30 innych osób (stan na 21.04.2016) było pomocnych?
dwanderson 21.04.16
0

Jest to bezpieczniejsze tylko pod warunkiem, że nie budujesz z -nie zawężaniem, jak powiedzmy, że Google robi w Chromium. Jeśli tak, to MNIEJ bezpieczniej. Bez tej flagi jedyne niebezpieczne przypadki zostaną naprawione przez C ++ 20.

Uwaga: A) Nawiasy klamrowe są bezpieczniejsze, ponieważ nie pozwalają na zwężenie. B) Nawiasy klamrowe są mniej bezpieczne, ponieważ mogą ominąć konstruktory prywatne lub usunięte i wywołać jawnie oznaczone konstruktory.

Te dwie kombinacje oznaczają, że są bezpieczniejsze, jeśli to, co jest w środku, jest prymitywnymi stałymi, ale mniej bezpieczne, jeśli są obiektami (chociaż ustalone w C ++ 20)

Allan Jensen
źródło
Próbowałem przekopać się na goldbolt.org, aby ominąć konstruktory „jawne” lub „prywatne” przy użyciu dostarczonego przykładowego kodu i uczynić jeden lub drugi prywatnym lub jawnym, i zostałem nagrodzony odpowiednim błędem kompilatora. Chcesz wykonać kopię zapasową za pomocą przykładowego kodu?
Mark Storer
To jest poprawka dla problemu zaproponowanego dla C ++ 20: open-std.org/jtc1/sc22/wg21/docs/papers/2018/p1008r1.pdf
Allan Jensen
1
Jeśli edytujesz swoją odpowiedź, aby pokazać, o której wersji C ++ mówisz, chętnie zmienię mój głos.
Mark Storer,