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:
vec3 GetValue()
{
return vec3(x, y, z);
}
Dlaczego muszę pisać vec3dwa 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ę:
vec3 GetValue()
{
return {x, y, z};
}
Wszystko działa poprawnie.
Jeszcze lepsze są argumenty funkcji. Rozważ to:
void DoSomething(const std::string &str);
DoSomething("A string.");
Działa to bez konieczności wpisywania nazwy typu, ponieważ std::stringwie, 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::stringKonstruktor, że trwa const 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 przypadku auto, możemy uniknąć dodatkowych nazw typów:
DoSomething({strValue, strLen});
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ło autobyć 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.
class Bar;
void Func()
{
int foo(Bar());
}
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 a Bar, a foozwracaną 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 nazwie Bar, co oznacza, że tworzy tymczasowy. Lub może to być funkcja, która nie przyjmuje parametrów i zwraca a Bar.
Jednolita inicjalizacja nie może być interpretowana jako prototyp funkcji:
class Bar;
void Func()
{
int foo{Bar{}};
}
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ęki Typename{}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.
int val{5.2};
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:
std::vector<int> v{100};
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 pobiera initializer_list<int>, a {100} może być poprawny initializer_list<int>, dlatego musi być .
Aby uzyskać konstruktora zmiany rozmiaru, musisz użyć ()zamiast {}.
Zauważ, że gdyby to było vectorcoś, czego nie można zamienić na liczbę całkowitą, to by się nie zdarzyło. Lista_inicjalizatora nie pasuje do konstruktora listy inicjalizatora tego vectortypu, a zatem kompilator będzie mógł wybierać spośród innych konstruktorów.