Rozumiem, że jednolita inicjalizacja C ++ 11 rozwiązuje pewną dwuznaczność składniową w języku, ale w wielu prezentacjach Bjarne Stroustrupa (szczególnie podczas rozmów GoingNative 2012) jego przykłady używają tej składni przede wszystkim teraz, gdy buduje obiekty.
Czy zaleca się teraz stosowanie jednolitej inicjalizacji we wszystkich przypadkach? Jakie powinno być ogólne podejście do tej nowej funkcji, jeśli chodzi o styl kodowania i ogólne zastosowanie? Jakie są powody, aby go nie używać?
Zauważ, że myślę przede wszystkim o konstrukcji obiektu jako moim przypadku użycia, ale jeśli są inne scenariusze do rozważenia, daj mi znać.
Odpowiedzi:
Styl kodowania jest ostatecznie subiektywny i jest bardzo mało prawdopodobne, aby przyniosło to znaczne korzyści w zakresie wydajności. Ale oto, co powiedziałbym, że zyskujesz dzięki swobodnemu stosowaniu jednolitej inicjalizacji:
Minimalizuje zbędne nazwy typów
Rozważ następujące:
Dlaczego muszę pisać
vec3
dwa razy? Czy ma to sens? Kompilator dobrze i dobrze wie, co zwraca funkcja. Dlaczego nie mogę po prostu powiedzieć „wezwać konstruktora tego, co zwracam, z tymi wartościami i zwrócić?” Dzięki jednolitej inicjalizacji mogę:Wszystko działa poprawnie.
Jeszcze lepsze są argumenty funkcji. Rozważ to:
Działa to bez konieczności wpisywania nazwy typu, ponieważ
std::string
wie, jak zbudować sięconst char*
niejawnie. To wspaniale. Ale co jeśli ten ciąg pochodzi, powiedz RapidXML. Lub sznur Lua. To znaczy, powiedzmy, że faktycznie znam długość sznurka z przodu.std::string
Konstruktor, że trwaconst char*
będą musiały podjąć długość napisu jeśli po prostu przejśćconst char*
.Istnieje przeciążenie, które wyraźnie trwa długo. Ale go używać, będę musiał to zrobić:
DoSomething(std::string(strValue, strLen))
. Dlaczego jest tam dodatkowa nazwa typu? Kompilator wie, jaki jest typ. Podobnie jak w przypadkuauto
, możemy uniknąć dodatkowych nazw typów:To po prostu działa. Bez nazwisk, bez zamieszania, nic. Kompilator wykonuje swoją pracę, kod jest krótszy i wszyscy są zadowoleni.
To prawda, że należy argumentować, że pierwsza wersja (
DoSomething(std::string(strValue, strLen))
) jest bardziej czytelna. To jest oczywiste, co się dzieje i kto co robi. W pewnym stopniu jest to prawdą; zrozumienie jednolitego kodu opartego na inicjalizacji wymaga spojrzenia na prototyp funkcji. Jest to ten sam powód, dla którego niektórzy twierdzą, że nigdy nie należy przekazywać parametrów przez odwołanie non-const: abyś mógł zobaczyć w witrynie wywoławczej, jeśli wartość jest modyfikowana.Ale to samo można powiedzieć
auto
; wiedza o tym, co otrzymujesz,auto v = GetSomething();
wymaga spojrzenia na definicjęGetSomething
. Ale to nie przestałoauto
być używane z niemal lekkomyślnym porzuceniem, gdy masz do niego dostęp. Osobiście uważam, że będzie dobrze, gdy się do tego przyzwyczaisz. Zwłaszcza z dobrym IDE.Nigdy nie otrzymuj najbardziej wekslowej analizy
Oto kod.
Pop quiz: co to jest
foo
? Jeśli odpowiedziałeś „zmienna”, jesteś w błędzie. W rzeczywistości jest to prototyp funkcji, która przyjmuje jako parametr funkcję, która zwraca aBar
, afoo
zwracaną wartością funkcji jest int.Nazywa się to „Most Vexing Parse” C ++, ponieważ nie ma absolutnie żadnego sensu dla istoty ludzkiej. Niestety zasady C ++ tego wymagają: jeśli można je interpretować jako prototyp funkcji, to tak właśnie będzie . Problemem jest
Bar()
; to może być jedna z dwóch rzeczy. Może to być typ o nazwieBar
, co oznacza, że tworzy tymczasowy. Lub może to być funkcja, która nie przyjmuje parametrów i zwraca aBar
.Jednolita inicjalizacja nie może być interpretowana jako prototyp funkcji:
Bar{}
zawsze tworzy tymczasowe.int foo{...}
zawsze tworzy zmienną.Istnieje wiele przypadków, w których chcesz użyć,
Typename()
ale po prostu nie można tego zrobić z powodu reguł analizy składniowej C ++. DziękiTypename{}
nie ma dwuznaczności.Powody, dla których nie
Jedyną prawdziwą mocą, którą oddajesz, jest zwężenie. Nie można zainicjować mniejszej wartości większą wartością o jednolitej inicjalizacji.
To się nie skompiluje. Możesz to zrobić za pomocą staromodnej inicjalizacji, ale nie jednolitej inicjalizacji.
Zostało to częściowo wykonane, aby listy inicjalizujące rzeczywiście działały. W przeciwnym razie byłoby wiele niejednoznacznych przypadków dotyczących typów list inicjalizacyjnych.
Oczywiście niektórzy mogą argumentować, że taki kod zasługuje na to, by się nie kompilować. Ja osobiście się zgadzam; zwężenie jest bardzo niebezpieczne i może prowadzić do nieprzyjemnego zachowania. Prawdopodobnie najlepiej uchwycić te problemy na wczesnym etapie kompilacji. Przynajmniej zawężenie sugeruje, że ktoś nie myśli zbyt mocno o kodzie.
Zauważ, że kompilatory zazwyczaj ostrzegają cię przed tego rodzaju rzeczami, jeśli poziom ostrzeżenia jest wysoki. Tak naprawdę to wszystko powoduje, że ostrzeżenie staje się wymuszonym błędem. Niektórzy mogą powiedzieć, że i tak powinieneś to robić;)
Jest jeszcze jeden powód, aby nie:
Co to robi? Może stworzyć
vector<int>
sto domyślnie skonstruowanych przedmiotów. Lub może stworzyćvector<int>
z 1 przedmiotem, który ma wartość100
. Oba są teoretycznie możliwe.W rzeczywistości robi to drugie.
Dlaczego? Listy inicjalizujące używają tej samej składni co jednolita inicjalizacja. Dlatego muszą istnieć pewne zasady wyjaśniające, co zrobić w przypadku niejasności. Zasada jest dość prosta: jeśli kompilator może użyć konstruktora listy inicjalizującej z listą inicjowaną nawiasami klamrowymi, zrobi to . Ponieważ
vector<int>
ma konstruktor listy inicjalizującej, który pobierainitializer_list<int>
, a {100} może być poprawnyinitializer_list<int>
, dlatego musi być .Aby uzyskać konstruktora zmiany rozmiaru, musisz użyć
()
zamiast{}
.Zauważ, że gdyby to było
vector
coś, czego nie można zamienić na liczbę całkowitą, to by się nie zdarzyło. Lista_inicjalizatora nie pasuje do konstruktora listy inicjalizatora tegovector
typu, a zatem kompilator będzie mógł wybierać spośród innych konstruktorów.źródło
std::vector<int> v{100, std::reserve_tag};
. Podobnie zstd::resize_tag
. Obecnie zajmuje dwa kroki, aby zarezerwować przestrzeń wektorową.int foo(10)
spotkałbyś tego samego problemu? Po drugie, kolejnym powodem, dla którego nie należy go używać, wydaje się być kwestia nadmiernej inżynierii, ale co, jeśli skonstruujemy wszystkie nasze obiekty przy użyciu{}
, ale dzień później w drodze dodam konstruktora do list inicjalizacji? Teraz wszystkie moje instrukcje konstrukcyjne zmieniają się w instrukcje listy inicjalizacyjnej. Wydaje się bardzo kruche pod względem refaktoryzacji. Jakieś komentarze na ten temat?int foo(10)
, czy nie napotkalibyście tego samego problemu?” Nr 10 to literał całkowity, a literał całkowity nigdy nie może być nazwą typu. Irytująca analiza wynika z faktu, żeBar()
może to być nazwa typu lub wartość tymczasowa. To powoduje niejednoznaczność kompilatora.unpleasant behavior
- jest nowy standardowy termin do zapamiętania:>Nie zgadzam się z sekcją odpowiedzi Nicola Bolasa Minimalizuje zbędne nazwy typowe . Ponieważ kod jest zapisywany jeden raz i czytany wiele razy, powinniśmy starać się zminimalizować czas potrzebny na odczyt i zrozumienie kodu, a nie czas potrzebny na napisanie kodu. Próba jedynie zminimalizowania pisania polega na próbie optymalizacji niewłaściwej rzeczy.
Zobacz następujący kod:
Ktoś czytający powyższy kod po raz pierwszy prawdopodobnie nie zrozumie instrukcji return, ponieważ zanim osiągnie ten wiersz, zapomni o typie zwrotu. Teraz będzie musiał przewinąć z powrotem do podpisu funkcji lub użyć funkcji IDE, aby zobaczyć typ zwracany i w pełni zrozumieć instrukcję return.
I tutaj znowu nie jest łatwo komuś czytając kod po raz pierwszy zrozumieć, co faktycznie jest konstruowane:
Powyższy kod się zepsuje, gdy ktoś zdecyduje, że DoSomething powinien również obsługiwać inny typ łańcucha i doda to przeciążenie:
Jeśli zdarzy się, że CoolStringType ma konstruktor, który przyjmuje const char * i size_t (podobnie jak std :: string), wówczas wywołanie DoSomething ({strValue, strLen}) spowoduje błąd niejednoznaczności.
Moja odpowiedź na aktualne pytanie:
Nie, nie należy traktować Uniform Inicjalizacji jako zamiennika składni starego konstruktora.
Moje rozumowanie jest następujące:
jeśli dwa stwierdzenia nie mają tego samego rodzaju intencji, nie powinny wyglądać tak samo. Istnieją dwa rodzaje inicjalizacji obiektu:
1) Weź wszystkie te przedmioty i wlej je do tego obiektu, który inicjuję.
2) Skonstruuj ten obiekt, korzystając z tych argumentów, które podałem jako przewodnik.
Przykłady zastosowania pojęcia nr 1:
Przykład zastosowania pojęcia nr 2:
Myślę, że to źle, że nowy standard pozwala ludziom inicjować schody w następujący sposób:
... ponieważ to zaciemnia znaczenie konstruktora. Taka inicjalizacja wygląda jak pojęcie nr 1, ale tak nie jest. Nie wlewa trzech różnych wartości wysokości kroku do obiektu, nawet jeśli tak wygląda. A także, co ważniejsze, jeśli opublikowano implementację biblioteki Stairs podobną do powyższej, a programiści jej używali, a następnie jeśli implementator biblioteki doda później konstruktora initializer_list do Stairs, wówczas cały kod, który używał Stairs z jednolitą inicjalizacją Składnia się zepsuje.
Uważam, że społeczność C ++ powinna zgodzić się na wspólną konwencję dotyczącą sposobu ujednoliconej inicjalizacji, tj. Równomiernie na wszystkich inicjalizacjach lub, jak sugeruję, oddzielenie tych dwóch pojęć inicjalizacji, a tym samym wyjaśnienie intencji programisty czytelnikowi kod.
AFTERTHOUGHT:
Oto jeszcze jeden powód, dla którego nie powinieneś myśleć o jednolitej inicjalizacji jako zamienniku starej składni i dlaczego nie możesz używać notacji nawiasów klamrowych dla wszystkich inicjalizacji:
Załóżmy, że preferowaną składnią do tworzenia kopii jest:
Teraz uważasz, że powinieneś zastąpić wszystkie inicjalizacje nową składnią nawiasów klamrowych, abyś mógł (i kod będzie wyglądać) bardziej spójny. Ale składnia wykorzystująca nawiasy klamrowe nie działa, jeśli typ T jest agregacją:
źródło
auto
w porównaniu z deklaracji wyraźny typu debaty, będę argumentować dla równowagi: jednolite inicjalizatory kołysać dość duży czas w szablonie meta-programowania sytuacjach, w których typ jest zazwyczaj dość oczywiste, w każdym razie. Pozwoli to uniknąć powtarzania twojego cholerstwa skomplikowanego-> decltype(....)
do inkantacji, np. Dla prostych szablonów funkcji oneline (doprowadziło mnie do płaczu).Jeśli konstruktory
merely copy their parameters
w odpowiednich klasach zmiennych,in exactly the same order
w których są zadeklarowane wewnątrz klasy, to użycie jednolitej inicjalizacji może ostatecznie być szybsze (ale może być również całkowicie identyczne) niż wywołanie konstruktora.Oczywiście nie zmienia to faktu, że zawsze musisz zadeklarować konstruktora.
źródło
struct X { int i; }; int main() { X x{42}; }
. Niepoprawne jest również to, że jednolita inicjalizacja może być szybsza niż inicjalizacja wartości.