wektor :: at a wektor :: operator []

95

Wiem, że at()jest to wolniejsze niż []ze względu na sprawdzanie granic, które jest również omawiane w podobnych pytaniach, takich jak C ++ Vector at / [] operator speed lub :: std :: vector :: at () vs operator [] << zaskakujące wyniki !! 5 do 10 razy wolniej / szybciej! . Po prostu nie rozumiem, do czego ta at()metoda jest dobra.

Jeśli mam prosty wektor taki jak ten: std::vector<int> v(10);i decyduję się uzyskać dostęp do jego elementów za pomocą at()zamiast []w sytuacji, gdy mam indeks ii nie jestem pewien, czy jest w granicach wektorów, zmusza mnie to do owinięcia go try-catch blok :

try
{
    v.at(i) = 2;
}
catch (std::out_of_range& oor)
{
    ...
}

chociaż jestem w stanie uzyskać to samo zachowanie, używając size()i sprawdzając indeks samodzielnie, co wydaje mi się łatwiejsze i znacznie wygodniejsze:

if (i < v.size())
    v[i] = 2;

Więc moje pytanie brzmi:
jakie są zalety używania vector :: at over vector :: operator [] ?
Kiedy powinienem używać vector :: at zamiast vector :: size + vector :: operator [] ?

LihO
źródło
11
+1 bardzo dobre pytanie !! ale nie sądzę, że w () jest to powszechnie używane.
Rohit Vipin Mathews,
10
Zwróć uwagę, że w Twoim przykładowym kodzie if (i < v.size()) v[i] = 2;istnieje możliwa ścieżka kodu, która w ogóle nie jest przypisana 2do żadnego elementu v. Jeśli to właściwe zachowanie, świetnie. Ale często nie ma nic sensownego, że ta funkcja może zrobić, kiedy i >= v.size(). Nie ma więc szczególnego powodu, dla którego nie powinien używać wyjątku do wskazania nieoczekiwanej sytuacji. Wiele funkcji po prostu używa operator[]bez sprawdzania rozmiaru, dokumentu, który imusi znajdować się w zasięgu, i obwinia wywołującego o wynikowy UB.
Steve Jessop,

Odpowiedzi:

74

Powiedziałbym, że wyjątki, które vector::at()rzucają, nie są tak naprawdę przeznaczone do przechwytywania przez bezpośrednio otaczający kod. Są one głównie przydatne do wyłapywania błędów w kodzie. Jeśli potrzebujesz sprawdzić ograniczenia w czasie wykonywania, ponieważ np. Indeks pochodzi z danych wejściowych użytkownika, rzeczywiście najlepiej będzie, jeśli skorzystasz z ifinstrukcji. Podsumowując, zaprojektuj swój kod z zamiarem, vector::at()który nigdy nie zgłosi wyjątku, więc jeśli tak się stanie, a program zostanie przerwany, jest to oznaka błędu. (tak jak assert())

pmdj
źródło
1
+1 Podoba mi się wyjaśnienie, jak oddzielić obsługę niewłaściwych danych wejściowych użytkownika (walidacja danych wejściowych; można się spodziewać nieprawidłowych danych wejściowych, więc nie jest to traktowane jako coś wyjątkowego) ... i błędy w kodzie (iterator wyłuskiwania, który jest poza zakresem, jest wyjątkowy rzecz)
Bojan Komazec
Mówisz więc, że powinienem używać size()+, []gdy indeks zależy od danych wejściowych użytkownika, używaj go assertw sytuacjach, w których indeks nigdy nie powinien znajdować się poza zakresem, aby łatwo naprawić błędy w przyszłości i .at()we wszystkich innych sytuacjach (na wszelki wypadek, ponieważ może się zdarzyć coś złego. .)
LihO,
8
@LihO: jeśli Twoja implementacja oferuje implementację debugowania vector, prawdopodobnie lepiej jest użyć jej jako opcji „na wszelki wypadek” niż at()wszędzie. W ten sposób możesz liczyć na nieco lepszą wydajność w trybie wydania, na wypadek, gdybyś kiedykolwiek tego potrzebował.
Steve Jessop,
3
Tak, większość dzisiejszych implementacji STL obsługuje tryb debugowania, który sprawdza nawet ograniczenia operator[], np. Gcc.gnu.org/onlinedocs/libstdc++/manual/, więc jeśli Twoja platforma to obsługuje, prawdopodobnie najlepiej będzie z tego korzystać!
pmdj
1
@pmdj fantastyczny punkt, o którym nie wiedziałem ... ale osierocony link. : P aktualny to: gcc.gnu.org/onlinedocs/libstdc++/manual/debug_mode.html
underscore_d
16

zmusza mnie do owinięcia go blokiem try-catch

Nie, nie (blok try / catch może znajdować się powyżej). Jest to przydatne, gdy chcesz, aby wyjątek został wyrzucony, a nie program, aby wejść w niezdefiniowaną dziedzinę zachowania.

Zgadzam się, że większość dostępów poza granicami do wektorów jest błędem programisty (w takim przypadku powinieneś użyć assertdo łatwiejszego zlokalizowania tych błędów; większość wersji bibliotek standardowych do debugowania robi to automatycznie). Nie chcesz używać wyjątków, które można połknąć w celu zgłaszania błędów programisty: chcesz mieć możliwość naprawienia błędu .

Ponieważ jest mało prawdopodobne, aby dostęp do wektora poza granicami był częścią normalnego przebiegu programu (w takim przypadku masz rację: sprawdź wcześniej za pomocą size zamiast pozwolić, aby wyjątek się pojawił), zgadzam się z twoją diagnostyką: atjest zasadniczo bezużyteczna.

Alexandre C.
źródło
Jeśli nie złapię out_of_rangewyjątku, abort()zostanie wywołany.
LihO,
@LihO: Niekoniecznie… try..catchmoże występować w metodzie, która wywołuje tę metodę.
Naveen,
12
Jeśli nic innego, atjest przydatne w takim stopniu, w jakim w innym przypadku piszesz coś takiego if (i < v.size()) { v[i] = 2; } else { throw what_are_you_doing_you_muppet(); }. Ludzie często myślą o funkcjach wyrzucających wyjątki w kategoriach „przekleństwa, muszę obsłużyć wyjątek”, ale tak długo, jak dokładnie dokumentujesz, co może rzucać każda z funkcji, można ich również używać jako „super, nie mam trzeba sprawdzić warunek i zgłosić wyjątek ”.
Steve Jessop,
@SteveJessop: Nie lubię rzucać wyjątków w przypadku błędów w programie, ponieważ mogą one zostać przechwycone przez innych programistów. Asercje są tutaj znacznie bardziej przydatne.
Alexandre C.
6
@AlexandreC. cóż, oficjalna odpowiedź jest taka, że out_of_rangepochodzi od logic_error, a inni programiści „powinni” wiedzieć lepiej, niż wychwytywać programy logic_errori ignorować je. assertmożna też zignorować, jeśli Twoi koledzy nie chcą wiedzieć o swoich błędach, jest to po prostu trudniejsze, ponieważ muszą kompilować Twój kod NDEBUG;-) Każdy mechanizm ma swoje zalety i wady.
Steve Jessop,
11

Jakie są zalety używania vector :: at over vector :: operator []? Kiedy powinienem używać vector :: at zamiast vector :: size + vector :: operator []?

Ważną kwestią jest tutaj to, że wyjątki pozwalają na oddzielenie normalnego przepływu kodu od logiki obsługi błędów, a pojedynczy blok catch może obsługiwać problemy generowane przez niezliczone miejsca przesyłania, nawet jeśli są rozproszone głęboko w wywołaniach funkcji. Tak więc at()niekoniecznie jest to łatwiejsze do pojedynczego użycia, ale czasami staje się łatwiejsze - i mniej zaciemniające logikę normalnych przypadków - gdy masz dużo indeksowania do sprawdzenia.

Warto również zauważyć, że w niektórych typach kodu indeks jest zwiększany w złożony sposób i stale używany do wyszukiwania tablicy. W takich przypadkach o wiele łatwiej jest zapewnić prawidłowe sprawdzenia za pomocą at().

Jako przykład ze świata rzeczywistego mam kod, który tokenizuje C ++ na elementy leksykalne, a następnie inny kod, który przesuwa indeks nad wektorem tokenów. W zależności od tego, co napotkano, mogę chcieć zwiększyć i sprawdzić następny element, na przykład:

if (token.at(i) == Token::Keyword_Enum)
{
    ASSERT_EQ(tokens.at(++i), Token::Idn);
    if (tokens.at(++i) == Left_Brace)
        ...
    or whatever

W takiej sytuacji bardzo trudno jest sprawdzić, czy nieprawidłowo dotarłeś do końca danych wejściowych, ponieważ jest to bardzo zależne od dokładnych napotkanych tokenów. Jawne sprawdzanie w każdym punkcie użycia jest bolesne i jest znacznie więcej miejsca na błędy programisty, gdy wkraczają inkrementacje przed / po, przesunięcia w miejscu użycia, błędne rozumowanie o ciągłej ważności niektórych wcześniejszych testów itp.

Tony Delroy
źródło
10

at może być wyraźniejsze, jeśli masz wskaźnik do wektora:

return pVector->at(n);
return (*pVector)[n];
return pVector->operator[](n);

Pomijając wydajność, pierwszym z nich jest prostszy i jaśniejszy kod.

Brangdon
źródło
... zwłaszcza gdy potrzebujesz wskaźnika do n-tego elementu wektora.
delfin
4

Po pierwsze, czy at()czy operator[]jest wolniejsze nie jest określona. Gdy nie ma błędu związanego z ograniczeniami, spodziewałbym się, że będą miały mniej więcej taką samą prędkość, przynajmniej w kompilacjach debugowania. Różnica polega na tym, że at()dokładnie określa, co się stanie, gdy wystąpi błąd granic (wyjątek), gdzie tak jak w przypadku operator[], jest to niezdefiniowane zachowanie - awaria we wszystkich systemach, których używam (g ++ i VC ++), przynajmniej wtedy, gdy używane są zwykłe flagi debugowania. (Inną różnicą jest to, że gdy jestem pewien swojego kodu, mogę uzyskać znaczny wzrost szybkości operator[] , wyłączając debugowanie. Jeśli wydajność tego wymaga - nie zrobiłbym tego, gdyby nie było to konieczne.)

W praktyce at()rzadko jest odpowiednie. Jeśli kontekst jest taki, że wiesz, że indeks może być nieprawidłowy, prawdopodobnie chcesz jawnego testu (np. Aby zwrócił wartość domyślną lub coś w tym rodzaju), a jeśli wiesz, że nie może być nieprawidłowy, chcesz przerwać (i jeśli nie wiesz, czy może być nieprawidłowy, czy nie, sugerowałbym dokładniejsze określenie interfejsu funkcji). Jest jednak kilka wyjątków, w których nieprawidłowy indeks może wynikać z analizy danych użytkownika, a błąd powinien spowodować przerwanie całego żądania (ale nie spowoduje wyłączenia serwera); w takich przypadkach wyjątek jest odpowiedni i at()zrobi to za Ciebie.

James Kanze
źródło
4
Dlaczego miałbyś oczekiwać, że będą poruszać się mniej więcej z tą samą prędkością, kiedy operator[]nie są zmuszeni do sprawdzania granicy , a tak at()jest? Czy przez to sugerujesz problemy z buforowaniem, spekulacjami i rozgałęzianiem buforów?
Sebastian Mach,
3
Przepraszamy, brakowało Twojego atrybutu „w trybie debugowania”. Jednak nie mierzyłbym kodu na podstawie jego jakości w trybie debugowania. W trybie zwolnienia sprawdzanie jest wymagane tylko przez at().
Sebastian Mach,
1
@phresnel Większość kodu, który dostarczyłem, była w trybie „debugowania”. Wyłączasz sprawdzanie tylko wtedy, gdy wymagają tego problemy z wydajnością. (Microsoft przed 2010 rokiem był tu trochę problemem, ponieważ std::stringnie zawsze działał, jeśli opcje sprawdzania nie odpowiadały opcjom środowiska wykonawczego: -MDi lepiej wyłącz sprawdzanie -MDd, i lepiej dalej.)
James Kanze,
3
Jestem bardziej z obozu, który mówi „kod jako usankcjonowany (gwarantowany) przez standard”; oczywiście możesz dostarczać w trybie debugowania, ale podczas tworzenia programów na wiele platform (w tym, ale nie wyłącznie, w przypadku tego samego systemu operacyjnego, ale różnych wersji kompilatora), poleganie na standardzie jest najlepszym rozwiązaniem w przypadku wydań i trybu debugowania jest uważany za narzędzie dla programisty, aby uzyskać to w większości poprawne i solidne :)
Sebastian Mach
1
Z drugiej strony, jeśli krytyczne części aplikacji są izolowane i chronione np. Przez zabezpieczenie wyjątków (RAII ftw), to czy każdy dostęp powinien operator[]zostać uszkodzony? Np std::vector<color> surface(witdh*height); ...; for (int y=0; y!=height; ++y).... Myślę, że wymuszanie sprawdzania granic dostarczonych plików binarnych podlega przedwczesnej pesymizacji. Imho, powinno to być tylko pomoc dla zespołu dla niezbyt dobrze zaprojektowanego kodu.
Sebastian Mach
1

Cały sens używania wyjątków polega na tym, że kod obsługi błędów może być dalej.

W tym konkretnym przypadku dane wprowadzane przez użytkownika są rzeczywiście dobrym przykładem. Wyobraź sobie, że chcesz semantycznie przeanalizować strukturę danych XML, która używa indeksów do odwoływania się do jakiegoś rodzaju zasobu, który przechowujesz wewnętrznie w pliku std::vector. Teraz drzewo XML jest drzewem, więc prawdopodobnie chcesz użyć rekurencji do jego analizy. W głębi, w rekurencji, może wystąpić naruszenie zasad dostępu ze strony autora pliku XML. W takim przypadku zazwyczaj chcesz usunąć wszystkie poziomy rekurencji i po prostu odrzucić cały plik (lub jakąkolwiek „grubszą” strukturę). Tutaj przydaje się at. Możesz po prostu napisać kod analizy tak, jakby plik był prawidłowy. Kod biblioteki zajmie się wykrywaniem błędów i możesz po prostu wyłapać błąd na poziomie zgrubnym.

Również inne kontenery, takie jak std::map, również mają std::map::atnieco inną semantykę niż std::map::operator[]: at mogą być używane na mapie const, podczas gdy operator[]nie mogą. Teraz, jeśli chcesz napisać kod niezależny od kontenera, na przykład coś, co mogłoby sobie poradzić z jednym const std::vector<T>&lub drugim const std::map<std::size_t, T>&, ContainerType::atbyłoby twoją bronią z wyboru.

Jednak wszystkie te przypadki pojawiają się zwykle podczas obsługi jakiegoś rodzaju niezatwierdzonych danych wejściowych. Jeśli jesteś pewien swojego prawidłowego zakresu, jak zwykle powinieneś, możesz zwykle użyć operator[], ale jeszcze lepiej, iteratorów z begin()i end().

ltjax
źródło
1

Zgodnie z tym artykułem, pomijając wydajność, nie ma znaczenia użycie atlub operator[]tylko wtedy, gdy gwarantuje się, że dostęp będzie mieścił się w rozmiarze wektora. W przeciwnym razie, jeśli dostęp jest oparty tylko na pojemności wektora, bezpieczniej jest z niego korzystać at.

ahj
źródło
2
tam są smoki. co się stanie, jeśli klikniemy ten link? (podpowiedź: już to wiem, ale na StackOverflow wolimy komentarze, które nie cierpią z powodu gnicia linków, tj. zawierają krótkie podsumowanie tego, co chcesz powiedzieć)
Sebastian Mach
Dzięki za wskazówkę. To jest teraz naprawione.
ahj
0

Uwaga: wygląda na to, że niektórzy nowi ludzie odrzucają tę odpowiedź bez uprzejmości, by powiedzieć, co jest nie tak. Poniższa odpowiedź jest poprawna i można ją tutaj zweryfikować .

Tak naprawdę jest tylko jedna różnica: atsprawdza granice, a operator[]nie. Dotyczy to zarówno kompilacji debugowania, jak i kompilacji wydań i jest to bardzo dobrze określone przez standardy. To takie proste.

Jest atto wolniejsza metoda, ale bardzo zła rada, aby jej nie używać at. Musisz patrzeć na liczby bezwzględne, a nie względne. Mogę się bezpiecznie założyć, że większość twojego kodu wykonuje grubo droższe operacje niż at. Osobiście staram się używać, atponieważ nie chcę, aby paskudny błąd powodował niezdefiniowane zachowanie i przedostał się do produkcji.

Shital Shah
źródło
1
Wyjątki w C ++ mają być mechanizmem obsługi błędów, a nie narzędziem do debugowania. Herb Sutter wyjaśnia dlaczego rzucania std::out_of_rangelub jakakolwiek forma std::logic_errorjest w rzeczywistości, błąd logiczny w sobie tutaj .
Big Temp
@BigTemp - Nie jestem pewien, jak Twój komentarz odnosił się do tego pytania i odpowiedzi. Tak, wyjątki są tematem wysoce dyskutowanym, ale tutaj chodzi o różnicę między ati, []a moja odpowiedź po prostu określa różnicę. Osobiście używam „bezpiecznej” metody, gdy perfekcyjny nie jest problemem. Jak mówi Knuth, nie rób przedwczesnej optymalizacji. Poza tym, bez względu na różnice filozoficzne, dobrze jest złapać błędy wcześniej niż w produkcji.
Shital Shah