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ć 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ę:
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::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 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 auto
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.
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 foo
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 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 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 tego vector
typu, a zatem kompilator będzie mógł wybierać spośród innych konstruktorów.