Widziałem konferencję Herb Suttera, w której zachęca on każdego programistę C ++ do korzystania auto
.
Jakiś czas temu musiałem przeczytać kod C #, gdzie var
był intensywnie używany i kod był bardzo trudny do zrozumienia - za każdym razem var
musiałem sprawdzać typ zwrotu po prawej stronie. Czasem więcej niż jeden raz, bo po pewnym czasie zapomniałem o typie zmiennej!
Wiem, że kompilator zna ten typ i nie muszę go pisać, ale powszechnie przyjmuje się, że powinniśmy pisać kod dla programistów, a nie dla kompilatorów.
Wiem też, że łatwiej jest napisać:
auto x = GetX();
Niż:
someWeirdTemplate<someOtherVeryLongNameType, ...>::someOtherLongType x = GetX();
Ale jest to zapisywane tylko raz, a GetX()
typ zwracany jest sprawdzany wiele razy, aby zrozumieć, jaki typ x
ma.
To mnie zastanawiało - czy auto
trudniej jest zrozumieć kod C ++?
auto
często sprawia, że rzeczy są trudniejsze do odczytania, gdy są już trudne do odczytania, tj. funkcje zbyt długie, zmienne źle nazwane itp. W przypadku krótkich funkcji z odpowiednio nazwanymi zmiennymi znajomość typów powinna być jedną z 1 łatwych lub 2 nieistotnych.auto
jest bardzo podobna do określania, kiedy użyćtypedef
. Od ciebie zależy, kiedy przeszkodzi, a kiedy pomoże.auto x = GetX();
, wybierz lepszą nazwę niż ta,x
która mówi ci, co robi w tym konkretnym kontekście ... i tak często jest bardziej przydatna niż jej typ.Odpowiedzi:
Krótka odpowiedź: Bardziej kompletnie, moim obecnym zdaniem
auto
jest to, że powinieneś używaćauto
domyślnie, chyba że wyraźnie chcesz konwersji. (Nieco bardziej precyzyjnie: „... chyba że chcesz jawnie zatwierdzić typ, co prawie zawsze dzieje się tak, ponieważ chcesz konwersji”).Dłuższa odpowiedź i uzasadnienie:
Napisz jawny typ (zamiast
auto
) tylko wtedy, gdy naprawdę chcesz jawnie zatwierdzić typ, co prawie zawsze oznacza, że chcesz jawnie uzyskać konwersję na ten typ. Z czubka głowy przypominam sobie dwa główne przypadki:initializer_list
Niespodzianka, któraauto x = { 1 };
wydedukowałainitializer_list
. Jeśli nie chceszinitializer_list
, powiedz typ - tzn. Wyraźnie poproś o konwersję.auto x = matrix1 * matrix 2 + matrix3;
przechwytuje typ pomocnika lub proxy, który nie powinien być widoczny dla programisty. W wielu przypadkach przechwytywanie tego typu jest w porządku i łagodne, ale czasami jeśli naprawdę chcesz, aby zwinął się i wykonał obliczenia, powiedz typ - tj. Ponownie wyraźnie poproś o konwersję.Rutynowo używaj
auto
domyślnie w przeciwnym razie, ponieważ użycieauto
pozwala uniknąć pułapek i sprawia, że kod jest bardziej poprawny, łatwiejszy w utrzymaniu i niezawodny oraz bardziej wydajny. Z grubsza w kolejności od najważniejszych do najmniej ważnych, w duchu „pisz najpierw dla jasności i poprawności”:auto
gwarancji otrzymasz odpowiedni typ. Jak to się mówi, jeśli powtórzysz się (powiedz typ niepotrzebnie), możesz i będziesz kłamać (źle to zrozumiesz). Oto zwykły przykład:void f( const vector<int>& v ) { for( /*…*
- w tym momencie, jeśli piszesz typ iteratora wprost, chcesz pamiętać, aby pisaćconst_iterator
(prawda?), Podczas gdyauto
po prostu robi to dobrze.auto
powoduje, że kod staje się bardziej niezawodny w obliczu zmiany, ponieważ gdy typ wyrażenia ulegnie zmianie,auto
nadal będzie rozpoznawany poprawny typ. Jeśli zamiast tego zdecydujesz się na typ jawny, zmiana typu wyrażenia wprowadzi ciche konwersje, gdy nowy typ konwertuje na stary typ, lub niepotrzebne przerwy w kompilacji, gdy nowy typ nadal działa podobnie jak stary typ, ale nie konwertuje na stary wpisz (na przykład, gdy zmieniszmap
na aunordered_map
, co jest zawsze w porządku, jeśli nie polegasz na zamówieniu, używającauto
iteratorów bezproblemowo przełączysz sięmap<>::iterator
naunordered_map<>::iterator
, ale używającmap<>::iterator
wszędzie wyraźnie oznacza to, że będziesz marnować swój cenny czas na falę poprawek kodu, chyba że stażysta przejdzie i nie będziesz w stanie nałożyć na nich nudnej pracy).auto
gwarantuje, że nie dojdzie do niejawnej konwersji, domyślnie gwarantuje lepszą wydajność. Jeśli zamiast tego powiesz typ, który wymaga konwersji, często po cichu uzyskasz konwersję, niezależnie od tego, czy tego oczekiwałeś, czy nie.auto
jest jedyną dobrą opcją dla trudnych do przeliterowania i niewymownych typów, takich jak lambdas i pomocniki szablonów, bez uciekania się do powtarzających siędecltype
wyrażeń lub mniej wydajnych pośrednich, takich jakstd::function
.auto
mniej pisania. Wspominam o tym jako o kompletności, ponieważ jest to powszechny powód, aby go lubić, ale nie jest to największy powód, aby go używać.Dlatego: Wolę powiedzieć
auto
domyślnie. Oferuje tyle prostoty, wydajności i przejrzystości, że ranisz siebie (i przyszłych opiekunów kodu) tylko wtedy, gdy tego nie zrobisz. Zatwierdź typ jawny tylko wtedy, gdy naprawdę masz na myśli, co prawie zawsze oznacza, że chcesz jawnej konwersji.Tak, jest (teraz) GotW na ten temat.
źródło
auto x = static_cast<X>(y)
.static_cast
Jasno wynika, że konwersja jest celowo i unika ostrzeżenia kompilatora o konwersji. Zwykle unikanie ostrzeżeń kompilatora nie jest tak dobre, ale nic mi nie jest, gdy nie otrzymuję ostrzeżenia o konwersji, które starannie rozważałem, piszącstatic_cast
. Chociaż nie zrobiłbym tego, gdyby nie było teraz ostrzeżeń, ale chcę otrzymywać ostrzeżenia w przyszłości, jeśli typy zmienią się w potencjalnie niebezpieczny sposób.auto
jest to, że powinniśmy starać się programować przeciwko interfejsom (nie w sensie OOP), a nie przeciwko konkretnym implementacjom. Tak samo jest z szablonami. Czy narzekasz na „trudny do odczytania kod”, ponieważ masz parametr typu szablonu,T
który jest używany wszędzie? Nie, nie sądzę. Również w szablonach kodujemy na interfejsie, wiele osób nazywa to pisaniem w czasie kompilacji.auto
.auto
zmiennej, ale prawie wszystkie z nich robią to poprawnie z wyraźną specyfikacją typu. Nikt nie używa IDE? Czy wszyscy używają tylko zmiennych int / float / bool? Czy wszyscy wolą zewnętrzną dokumentację bibliotek zamiast samodokumentowanych nagłówków?=
RHS nie mają większego sensu w żadnej innej interpretacji (lista inicjująca z prętami, ale musisz wiedzieć, co inicjujesz, z czym jest oksymoronemauto
). Ten, który jest zaskakujący,auto i{1}
również wydedukujeinitializer_list
, pomimo sugerowania, że nie bierz tej spiętej listy inicjującej, ale raczej weź to wyrażenie i użyj jego typu ... ale myinitializer_list
też tam jesteśmy . Na szczęście C ++ 17 dobrze to naprawia.Jest to sytuacja indywidualna.
Czasami utrudnia to zrozumienie kodu, a czasem nie. Weź na przykład:
jest zdecydowanie łatwy do zrozumienia i zdecydowanie łatwiejszy do napisania niż rzeczywista deklaracja iteratora.
Używam C ++ już od jakiegoś czasu, ale mogę zagwarantować, że przy pierwszym uruchomieniu dostanę błąd kompilatora, ponieważ zapomnę o tym
const_iterator
i początkowo pójdę naiterator
... :)Użyłbym tego do takich przypadków, ale nie tam, gdzie faktycznie zaciemnia typ (jak twoja sytuacja), ale jest to czysto subiektywne.
źródło
std::map<int, std::string>::const_iterator
, więc to nie tak, że nazwa i tak wiele mówi o typie.int
, a wartość jeststd::string
. :)it->second
ponieważ jest to ciągły iterator. Wszystkie informacje, które jest powtórzeniem tego, co znajduje się w poprzednim wierszuconst std::map<int, std::string>& x
. Wielokrotne mówienie rzeczy czasami lepiej informuje, ale w żadnym wypadku nie jest to ogólna zasada :-)for (anX : x)
sprawić, by stało się jeszcze bardziej oczywiste, że właśnie się powtarzamyx
. Normalnym przypadkiem, w którym potrzebujesz iteratora, jest modyfikowanie kontenera, alex
jest toconst&
Spójrz na to z innej strony. Czy ty piszesz:
lub:
Czasami nie pomaga to przeliterować tego typu.
Decyzja, czy należy podać typ, nie jest tym samym, co decyzja o podzieleniu kodu na wiele instrukcji poprzez zdefiniowanie zmiennych pośrednich. W C ++ 03 oba były ze sobą powiązane, można pomyśleć o
auto
sposobie ich rozdzielenia.Czasami przydatne może być wyraźne określenie typów:
vs.
W przypadkach, w których deklarujesz zmienną, użycie
auto
pozwala na wypisanie typu niewypowiedzianego, tak jak w wielu wyrażeniach. Prawdopodobnie powinieneś sam zdecydować, kiedy to poprawi czytelność, a kiedy utrudni.Można argumentować, że mieszanie typów podpisanych i niepodpisanych jest błędem na początku (w rzeczywistości niektórzy twierdzą dalej, że nie należy w ogóle używać typów niepodpisanych). Powodem jest zapewne błędem jest to, że sprawia, że typy argumentów niezwykle ważne ze względu na różne zachowania. Jeśli źle jest znać typy swoich wartości, prawdopodobnie nie jest też złą rzeczą nie znać ich. Więc pod warunkiem, że kod nie jest już mylący z innych powodów, to czyni
auto
OK, prawda? ;-)Szczególnie przy pisaniu kodu ogólnego zdarzają się przypadki, w których faktyczny typ zmiennej nie powinien być ważny, ważne jest to, że spełnia on wymagany interfejs. Więc
auto
zapewnia poziom abstrakcji gdzie zignorować typ (ale oczywiście kompilator nie, to wie). Praca na odpowiednim poziomie abstrakcji może znacznie poprawić czytelność, praca na „złym” poziomie sprawia, że czytanie kodu jest hasłem.źródło
auto
pozwala na tworzenie nazwanych zmiennych o nienazwanych lub nieciekawych typach. Znaczące nazwy mogą być przydatne.sizeof
na tobie szaloną definicję „ bez znaku”.IMO, patrzysz na to prawie odwrotnie.
Nie chodzi o
auto
doprowadzenie do kodu, który jest nieczytelny lub nawet mniej czytelny. Jest to kwestia (mając nadzieję, że) wyraźnego typu wartości zwracanej zrekompensuje fakt, że (najwyraźniej) nie jest jasne, jaki typ zostałby zwrócony przez jakąś określoną funkcję.Przynajmniej moim zdaniem, jeśli masz funkcję, której typ zwrotu nie jest od razu oczywisty, to właśnie tam masz problem. To, co robi funkcja, powinno wynikać z jej nazwy, a rodzaj zwracanej wartości powinien wynikać z jej działania. Jeśli nie, to prawdziwe źródło problemu.
Jeśli jest tu problem, to nie jest
auto
. Z resztą kodu i są całkiem spore szanse, że jawny typ jest wystarczającą pomocą pasma, aby powstrzymać cię od dostrzeżenia i / lub naprawienia podstawowego problemu. Po naprawieniu tego prawdziwego problemu czytelność używanego koduauto
będzie na ogół w porządku.Przypuszczam, że uczciwie powinienem dodać: miałem do czynienia z kilkoma przypadkami, w których takie rzeczy nie były tak oczywiste, jak byś chciał, a naprawienie problemu również było nie do zniesienia. Dla przykładu, kilka lat temu przeprowadziłem konsultacje dla firmy, która wcześniej połączyła się z inną firmą. W rezultacie powstała baza kodu, która była bardziej „pchana razem” niż połączona. Programy składowe zaczęły używać różnych (ale całkiem podobnych) bibliotek do podobnych celów, i chociaż pracowały nad tym, aby scalić wszystko w bardziej przejrzysty sposób, nadal tak było. W sporej liczbie przypadków jedynym sposobem na odgadnięcie, jaki typ zostanie zwrócony przez daną funkcję, było poznanie jej pochodzenia.
Nawet w takim przypadku możesz pomóc w wyjaśnieniu kilku rzeczy. W takim przypadku cały kod zaczął się w globalnej przestrzeni nazw. Po prostu przeniesienie sporej kwoty do niektórych przestrzeni nazw wyeliminowało konflikty nazw i znacznie ułatwiło śledzenie typów.
źródło
Istnieje kilka powodów, dla których nie lubię auto do ogólnego użytku:
Ale czekaj, czy to naprawdę dobry pomysł? Co jeśli typ miał znaczenie w pół tuzinie przypadków użycia, a teraz ten kod zachowuje się inaczej? Może to również pośrednio przerwać enkapsulację, modyfikując nie tylko wartości wejściowe, ale także samo zachowanie prywatnej implementacji innych klas wywołujących funkcję.
1a. Jestem zwolennikiem koncepcji „samodokumentującego się kodu”. Kod samo-dokumentujący ma swoje uzasadnienie w tym, że komentarze stają się nieaktualne, nie odzwierciedlają już tego, co robi kod, podczas gdy sam kod - jeśli jest napisany w sposób jawny - jest zrozumiały, zawsze jest aktualny zgodnie z jego intencją i nie pozostawi cię mylonych ze starymi komentarzami. Jeśli typy można zmienić bez potrzeby modyfikowania samego kodu, wówczas sam kod / zmienne mogą stać się nieaktualne. Na przykład:
auto bThreadOK = CheckThreadHealth ();
Tyle tylko, że problem polega na tym, że CheckThreadHealth () w pewnym momencie został refaktoryzowany w celu zwrócenia wartości wyliczeniowej wskazującej ewentualny status błędu zamiast bool. Ale osoba, która wprowadziła tę zmianę, nie sprawdzała tego konkretnego wiersza kodu, a kompilator nie pomógł, ponieważ skompilował się bez ostrzeżeń i błędów.
Prawdopodobnie nawet działa. Mówię, że to działa, ponieważ chociaż tworzysz kopię struktury 500-bajtowej dla każdej iteracji pętli, więc możesz sprawdzić na niej jedną wartość, kod jest nadal w pełni funkcjonalny. Więc nawet testy jednostkowe nie pomagają zrozumieć, że za tym prostym i niewinnie wyglądającym auto kryje się zły kod. Większość innych osób skanujących plik również nie zauważy go na pierwszy rzut oka.
Można to również pogorszyć, jeśli nie wiesz, jaki jest typ, ale wybierasz nazwę zmiennej, która błędnie zakłada, co to jest, w efekcie osiągając taki sam wynik jak w 1a, ale od samego początku, a nie postfaktor.
Wydaje mi się oczywiste, że auto zostało wprowadzone przede wszystkim jako obejście strasznej składni ze standardowymi typami szablonów bibliotek. Zamiast próbować naprawić składnię szablonu, którą ludzie już znają - co może być prawie niemożliwe do wykonania z powodu całego istniejącego kodu, który mógłby złamać - dodaj słowo kluczowe, które w zasadzie ukrywa problem. Zasadniczo to, co możesz nazwać „hack”.
Właściwie nie mam żadnych sporów z użyciem auto ze standardowymi kontenerami bibliotecznymi. Oczywiście dla tego słowa kluczowego zostało utworzone, a funkcje w standardowej bibliotece prawdopodobnie nie zmienią się zasadniczo w celu (lub typowaniu w tym zakresie), dzięki czemu korzystanie z auto jest względnie bezpieczne. Byłbym jednak bardzo ostrożny w używaniu go z własnym kodem i interfejsami, które mogą być znacznie bardziej zmienne i potencjalnie podlegać bardziej fundamentalnym zmianom.
Inną użyteczną aplikacją auto, która zwiększa możliwości języka, jest tworzenie tymczasowych makr typu agnostic. To było coś, czego tak naprawdę nie mogłeś zrobić wcześniej, ale możesz to zrobić teraz.
źródło
auto something = std::make_shared<TypeWithLongName<SomeParam>>(a,b,c);
. :-)Tak, łatwiej jest poznać typ zmiennej, jeśli nie jest używana
auto
. Pytanie brzmi: czy musisz znać typ swojej zmiennej, aby odczytać kod? Czasami odpowiedź będzie twierdząca, a czasem nie. Na przykład, kiedy otrzymujesz iterator zstd::vector<int>
, czy musisz wiedzieć, że jest tostd::vector<int>::iterator
lubauto iterator = ...;
wystarczy? Wszystko, co każdy chciałby zrobić z iteratorem, wynika z faktu, że jest to iterator - nie ma znaczenia, jaki typ jest konkretny.Używaj
auto
w sytuacjach, gdy nie utrudnia to odczytania kodu.źródło
Osobiście używam
auto
tylko wtedy, gdy jest to absolutnie oczywiste dla programisty, co to jest.Przykład 1
Przykład 2
źródło
auto record = myObj.FindRecord(something)
, byłoby jasne, że typ zmiennej był rekordowy. Lub nazwanie goit
lub podobnego sprawi, że będzie jasne, że zwraca iterator. Zauważ, że nawet jeśli nie używałeśauto
, prawidłowe nazewnictwo zmiennej oznaczałoby, że nie musisz wracać do deklaracji, aby spojrzeć na typ z dowolnego miejsca w funkcji . Usunąłem moją opinię, ponieważ ten przykład nie jest teraz kompletnym głupkiem, ale nadal nie kupuję tutaj argumentu.MyClass::RecordTy record = myObj.FindRecord (something)
To pytanie wymaga opinii, które będą się różnić w zależności od programisty, ale powiedziałbym, że nie. W rzeczywistości w wielu przypadkach wręcz przeciwnie,
auto
może pomóc w zrozumieniu kodu, pozwalając programiście skupić się na logice, a nie na szczegółach.Jest to szczególnie prawdziwe w przypadku złożonych typów szablonów. Oto uproszczony i przemyślany przykład. Co jest łatwiejsze do zrozumienia?
.. lub ...
Niektórzy twierdzą, że drugi jest łatwiejszy do zrozumienia, inni mogą powiedzieć pierwszy. Jeszcze inni mogą powiedzieć, że nieuzasadnione użycie
auto
może przyczynić się do ogłuszenia programistów, którzy go używają, ale to już inna historia.źródło
std::map
przykłady, dodatkowo ze złożonymi argumentami szablonu.map
s. :)for
np. Jeśli iteratory są unieważnione w ciele pętli i dlatego muszą być wstępnie zwiększane lub w ogóle nie zwiększane.Wiele dobrych odpowiedzi do tej pory, ale aby skupić się na pierwotnym pytaniu, myślę, że Herb posuwa się za daleko w swoich radach, aby korzystać z niego
auto
swobodnie. Twój przykład to jeden przypadek, w którym użycieauto
oczywiście szkodzi czytelności. Niektórzy twierdzą, że nie ma problemu z nowoczesnymi IDE, w których można najechać wskaźnikiem na zmienną i zobaczyć typ, ale nie zgadzam się: nawet ludzie, którzy zawsze używają IDE, czasami muszą patrzeć na fragmenty kodu w izolacji (pomyśl o recenzjach kodu , na przykład), a IDE nie pomoże.Konkluzja: użyj,
auto
gdy pomaga: tj. Iteratory dla pętli. Nie używaj go, gdy zmusza czytelnika do znalezienia typu.źródło
Jestem dość zaskoczony, że nikt jeszcze nie zauważył, że auto pomaga, jeśli nie ma wyraźnego typu. W takim przypadku możesz albo obejść ten problem, używając #define lub typedef w szablonie, aby znaleźć rzeczywisty typ użyteczny (i czasami nie jest to trywialne), lub po prostu użyj auto.
Załóżmy, że masz funkcję, która zwraca coś z typem platformy:
Wolisz używać czarownic?
lub po prostu
Jasne, możesz pisać
gdzieś też, ale robi
faktycznie powiedzieć coś więcej o typie x? Mówi, że jest to, co jest stamtąd zwracane, ale jest dokładnie tym, czym jest auto. Jest to po prostu zbędne - „rzeczy” są tutaj pisane 3 razy - to moim zdaniem czyni go mniej czytelnym niż wersja „auto”.
źródło
typedef
nich.Czytelność jest subiektywna; musisz spojrzeć na sytuację i zdecydować, co jest najlepsze.
Jak zauważyłeś, bez auto długie deklaracje mogą powodować dużo bałaganu. Jednak, jak wskazałeś, krótkie deklaracje mogą usuwać informacje o typie, które mogą być cenne.
Ponadto dodam jeszcze: upewnij się, że patrzysz na czytelność, a nie na pisalność. Kod, który łatwo jest napisać, ogólnie nie jest łatwy do odczytania i odwrotnie. Na przykład, gdybym pisał, wolałbym auto. Gdybym czytał, może dłuższe deklaracje.
Potem jest konsekwencja; jak to dla ciebie ważne? Czy chciałbyś mieć auto w niektórych częściach i wyraźne deklaracje w innych, czy jedną spójną metodę?
źródło
Zaletą będzie mniej czytelny kod i zachęcę programistę do korzystania z niego coraz częściej. Dlaczego? Oczywiście, jeśli kod korzystający z auto jest trudny do odczytania, wówczas trudno będzie również napisać. Programista jest zmuszony użyć znaczącej nazwy zmiennej , aby poprawić swoją pracę.
Być może na początku programista może nie pisać znaczących nazw zmiennych. Ale ostatecznie, gdy naprawia błędy lub podczas przeglądu kodu, gdy musi wyjaśnić kod innym lub w niedalekiej przyszłości, wyjaśniając kod konserwatorom, programista zda sobie sprawę z błędu i użyje znacząca nazwa zmiennej w przyszłości.
źródło
myComplexDerivedType
celu uzupełnienia brakującego typu, co zaśmieca kod powtarzaniem typu (wszędzie, gdzie używana jest zmienna) i zachęca ludzi do pominięcia celu zmiennej w jej nazwie . Z mojego doświadczenia wynika, że nie ma nic tak nieproduktywnego jak aktywne stawianie przeszkód w kodzie.Mam dwie wskazówki:
Jeśli typ zmiennej jest oczywisty, żmudne w pisaniu lub trudne do ustalenia użyj auto.
Jeśli potrzebujesz konkretnej konwersji lub typ wyniku nie jest oczywisty i może powodować zamieszanie.
źródło
Tak. Zmniejsza to gadatliwość, ale częstym nieporozumieniem jest to, że gadatliwość zmniejsza czytelność. Jest to prawdą tylko wtedy, gdy uważasz, że czytelność jest bardziej estetyczna niż faktyczna zdolność do interpretowania kodu - która nie jest zwiększana przez użycie auto. W najczęściej cytowanym przykładzie, iteratorach wektorowych, na powierzchni może się wydawać, że użycie auto zwiększa czytelność kodu. Z drugiej strony nie zawsze wiesz, co da ci słowo kluczowe auto. Musisz wykonać tę samą logiczną ścieżkę, co kompilator, aby dokonać wewnętrznej rekonstrukcji i przez większość czasu, szczególnie w przypadku iteratorów, będziesz przyjmować błędne założenia.
Pod koniec dnia „auto” poświęca czytelność kodu i klarowność, dla syntaktycznej i estetycznej „czystości” (co jest konieczne tylko dlatego, że iteratory mają niepotrzebnie skomplikowaną składnię) oraz możliwość wpisania 10 mniej znaków w dowolnym wierszu. Nie warto ryzykować ani długofalowego wysiłku.
źródło