Słowo noexcept
kluczowe można odpowiednio zastosować do wielu podpisów funkcji, ale nie jestem pewien, kiedy powinienem rozważyć jego użycie w praktyce. Na podstawie tego, co przeczytałem do tej pory, dodanie w ostatniej chwili noexcept
wydaje się odnosić do niektórych ważnych problemów, które pojawiają się, gdy rzucają konstruktorzy ruchu. Nadal jednak nie jestem w stanie udzielić satysfakcjonujących odpowiedzi na niektóre praktyczne pytania, które skłoniły mnie do przeczytania noexcept
przede wszystkim.
Istnieje wiele przykładów funkcji, o których wiem, że nigdy nie wyrzucą ich, ale dla których kompilator nie jest w stanie tego ustalić samodzielnie. Czy
noexcept
we wszystkich takich przypadkach powinienem dołączyć deklarację funkcji ?Myślenie o tym, czy muszę dołączać
noexcept
po każdej deklaracji funkcji, znacznie zmniejszyłoby produktywność programisty (i szczerze mówiąc, byłby to problem w dupie). W jakich sytuacjach powinienem być bardziej ostrożny w używaniunoexcept
i dla jakich sytuacji mogę uciec od dorozumianychnoexcept(false)
?Kiedy realistycznie mogę spodziewać się poprawy wydajności po użyciu
noexcept
? W szczególności podaj przykład kodu, dla którego kompilator C ++ jest w stanie wygenerować lepszy kod maszynowy po dodaniunoexcept
.Osobiście zależy mi
noexcept
na zwiększonej swobodzie kompilatora w zakresie bezpiecznego stosowania pewnych rodzajów optymalizacji. Czy nowoczesne kompilatory korzystająnoexcept
w ten sposób? Jeśli nie, czy mogę oczekiwać, że niektóre z nich to zrobią w najbliższej przyszłości?
move_if_nothrow
(lub whatchamacallit) zobaczy poprawę wydajności, jeśli nie ma wyjątku ctor ruchu.move_if_noexcept
.Odpowiedzi:
Myślę, że jest za wcześnie, aby udzielić odpowiedzi na „najlepsze praktyki”, ponieważ nie było wystarczająco dużo czasu, aby wykorzystać je w praktyce. Gdyby zapytano o specyfikatory rzutów zaraz po ich wyjściu, odpowiedzi byłyby zupełnie inne niż teraz.
Cóż, użyj go, gdy jest oczywiste, że funkcja nigdy nie rzuci.
Wydaje się, że największe korzyści optymalizacji wynikają z optymalizacji użytkowników, a nie z kompilacji, z uwagi na możliwość sprawdzenia
noexcept
i przeciążenia. Większość kompilatorów stosuje metodę obsługi wyjątków bez kar, jeśli nie wyrzucisz, więc wątpię, czy zmieniłaby wiele (lub cokolwiek) na poziomie kodu maszynowego twojego kodu, chociaż być może zmniejszy rozmiar pliku binarnego poprzez usunięcie kod obsługi.Zastosowanie
noexcept
w wielkiej czwórce (konstruktory, przypisanie, a nie destrukty, jak już sąnoexcept
) prawdopodobnie spowoduje najlepsze ulepszenia, ponieważnoexcept
kontrole są „powszechne” w kodzie szablonu, np. Wstd
kontenerach. Na przykładstd::vector
nie użyje ruchu twojej klasy, chyba że jest zaznaczonynoexcept
(lub kompilator może wydedukować go inaczej).źródło
std::terminate
sztuczka nadal jest zgodna z modelem zerowego kosztu. Oznacza to, że tak się składa, że zakres instrukcji w ramachnoexcept
funkcji jest odwzorowany na wywołanie,std::terminate
jeślithrow
zostanie użyte zamiast rozwijania stosu. W związku z tym wątpię, by miało więcej narzutu niż zwykłe śledzenie wyjątków.noexcept
gwarantuje to.noexcept
funkcja rzuca, wówczasstd::terminate
wywoływana jest, co wydaje się, że wiązałoby się to z niewielkim narzutem”… Nie, należy to zaimplementować, nie generując tabel wyjątków dla takiej funkcji, które dyspozytor wyjątków powinien złapać, a następnie wyskoczyć.noexcept
jest częścią interfejsu funkcji ; nie powinieneś go dodawać tylko dlatego, że twoja obecna implementacja się nie rzuca. Nie jestem pewien właściwej odpowiedzi na to pytanie, ale jestem całkiem pewien, że to, jak zachowuje się twoja funkcja dzisiaj, nie ma z tym nic wspólnego ...Powtarzam te dni: najpierw semantyka .
Dodawanie
noexcept
,noexcept(true)
inoexcept(false)
to przede wszystkim o semantyce. Tylko przypadkowo warunkuje szereg możliwych optymalizacji.Jako programista czytający kod, obecność
noexcept
jest podobna doconst
: pomaga mi lepiej zrozumieć, co może się zdarzyć, a co nie. Dlatego warto poświęcić trochę czasu na zastanowienie się, czy wiesz, czy funkcja rzuci. Dla przypomnienia może zostać rzucony dowolny rodzaj dynamicznej alokacji pamięci.Dobra, teraz przejdźmy do możliwych optymalizacji.
Najbardziej oczywiste optymalizacje są przeprowadzane w bibliotekach. C ++ 11 zapewnia szereg cech, które pozwalają wiedzieć, czy dana funkcja jest,
noexcept
czy nie, a sama implementacja biblioteki standardowej wykorzysta te cechy, aby faworyzowaćnoexcept
operacje na obiektach zdefiniowanych przez użytkownika, jeśli to możliwe. Takich jak semantyka ruchu .Kompilator może ogolić tylko trochę tłuszczu (być może) z danych obsługujących wyjątki, ponieważ musi wziąć pod uwagę fakt, że mogłeś skłamać. Jeśli zaznaczona funkcja
noexcept
rzuca,std::terminate
jest wywoływana.Te semantyki zostały wybrane z dwóch powodów:
noexcept
nawet wtedy, gdy zależności nie wykorzystują go już (kompatybilność wsteczna)noexcept
wywoływania funkcji, które mogą teoretycznie rzucać, ale nie są oczekiwane dla podanych argumentówźródło
noexcept
funkcje, nie musiałaby robić nic specjalnego, ponieważ wszelkie wyjątki, które mogą się pojawić, uruchamiają się,terminate
zanim osiągną ten poziom. Różni się to znacznie od konieczności radzenia sobie i propagowaniabad_alloc
wyjątku.noexcept
jest wyraźnie pomocny / dobry pomysł. Zaczynam myśleć, że ruch budowy, przydział ruchu i zamiana to jedyne przypadki, które istnieją ... Czy znasz jakieś inne?noexcept
możliwe, ktoś może go pewnie wykorzystać na kawałku danych, do którego można później uzyskać dostęp. Widziałem, że ten pomysł jest używany gdzie indziej, ale biblioteka Standard jest raczej cienka w C ++ i służy tylko do optymalizacji kopii elementów, jak sądzę.To faktycznie robi (potencjalnie) ogromną różnicę w stosunku do optymalizatora w kompilatorze. Kompilatory mają tę funkcję od lat za pomocą instrukcji pustego throw () po definicji funkcji, a także rozszerzeń właściwości. Mogę zapewnić, że nowoczesne kompilatory wykorzystują tę wiedzę do generowania lepszego kodu.
Prawie każda optymalizacja w kompilatorze wykorzystuje coś, co nazywa się „wykresem przepływu” funkcji do uzasadnienia tego, co jest legalne. Wykres przepływu składa się z tak zwanych „bloków” funkcji (obszary kodu, które mają pojedyncze wejście i jedno wyjście) i krawędzi między blokami, aby wskazać, gdzie przepływ może się przeskoczyć. Noexcept zmienia wykres przepływu.
Poprosiłeś o konkretny przykład. Rozważ ten kod:
Wykres przepływu dla tej funkcji jest inny, jeśli
bar
jest oznaczonynoexcept
(nie ma sposobu, aby wykonanie przeskakiwało między końcembar
a instrukcją catch). Gdy oznaczony jakonoexcept
, kompilator ma pewność, że wartość x wynosi 5 podczas funkcji baz - mówi się, że blok x = 5 „dominuje” blok baz (x) bez krawędzi odbar()
do instrukcji catch.Następnie może zrobić coś, co nazywa się „stałą propagacją”, aby wygenerować bardziej wydajny kod. W tym przypadku, jeśli baz jest wbudowany, instrukcje używające x mogą również zawierać stałe, a to, co kiedyś było oceną środowiska wykonawczego, może zostać przekształcone w ocenę czasu kompilacji itp.
W każdym razie krótka odpowiedź:
noexcept
pozwala kompilatorowi wygenerować ściślejszy wykres przepływu, a wykres przepływu służy do uzasadnienia wszelkiego rodzaju typowych optymalizacji kompilatora. Dla kompilatora adnotacje użytkownika tego rodzaju są niesamowite. Kompilator spróbuje to rozgryźć, ale zwykle nie może (funkcja, o której mowa, może znajdować się w innym pliku obiektowym niewidocznym dla kompilatora lub przejściowo użyć funkcji, która nie jest widoczna), lub gdy to robi, istnieje jakiś trywialny wyjątek, który może zostaćnoexcept
zgłoszony, o którym nawet nie zdajesz sobie sprawy, więc nie można go niejawnie oznaczyć jako (na przykład przydzielenie pamięci może spowodować błąd bad_alloc).źródło
x = 5
może rzucić. Gdyby ta częśćtry
bloku służyła jakiemukolwiek celowi, rozumowanie nie miałoby sensu.throw
instrukcji lub innych podobnych rzeczynew
. Jeśli kompilator nie widzi ciała, musi polegać na obecności lub nieobecnościnoexcept
. Zwykły dostęp do tablicy zwykle nie generuje wyjątków (C ++ nie ma sprawdzania granic), więc nie, dostęp do tablicy sam w sobie nie spowodowałby, że kompilator pomyśli, że funkcja zgłasza wyjątki. (Dostęp pozathrow()
w C ++ przed-noexceptnoexcept
może znacznie poprawić wydajność niektórych operacji. Nie dzieje się tak na poziomie generowania kodu maszynowego przez kompilator, ale poprzez wybranie najbardziej efektywnego algorytmu: jak wspomniano inni, wybór ten wykonuje się za pomocą funkcjistd::move_if_noexcept
. Na przykład wzroststd::vector
(np. Kiedy dzwonimyreserve
) musi zapewniać silną gwarancję bezpieczeństwa wyjątkowego. Jeśli wie, żeT
konstruktor ruchu nie rzuca, może po prostu przenieść każdy element. W przeciwnym razie musi skopiować wszystkieT
s. Zostało to szczegółowo opisane w tym poście .źródło
noexcept
do nich operatory przypisania (jeśli dotyczy)! Niejawnie zdefiniowane funkcje elementów przenoszących zostałynoexcept
do nich dodane automatycznie (jeśli dotyczy).Hm, nigdy? Czy nigdy nie jest czas Nigdy.
noexcept
służy do optymalizacji wydajności kompilatora w taki sam sposób, jakconst
do optymalizacji wydajności kompilatora. To znaczy prawie nigdy.noexcept
służy przede wszystkim do umożliwienia „tobie” wykrycia w czasie kompilacji, czy funkcja może zgłosić wyjątek. Pamiętaj: większość kompilatorów nie emituje specjalnego kodu dla wyjątków, chyba że faktycznie coś zgłasza. Więcnoexcept
nie jest to kwestia dawania wskazówek kompilator o tym, jak zoptymalizować funkcję tyle dając Ci wskazówki o tym, jak korzystać z funkcji.Szablony takie
move_if_noexcept
wykryją, czy konstruktor ruchu jest zdefiniowany za pomocąnoexcept
i zwrócąconst&
zamiast&&
typu, jeśli nie jest. Jest to sposób na powiedzenie ruchu, jeśli jest to bardzo bezpieczne.Zasadniczo powinieneś używać tego,
noexcept
kiedy uważasz, że będzie to przydatne . Niektóre kody będą miały różne ścieżki, jeśliis_nothrow_constructible
są prawdziwe dla tego typu. Jeśli używasz kodu, który to zrobi, zachęcamynoexcept
odpowiednich konstruktorów.W skrócie: używaj go do konstruktorów ruchów i podobnych konstrukcji, ale nie czujesz, że musisz się z tym szaleć.
źródło
move_if_noexcept
nie zwróci kopii, zwróci stałe odwołanie do wartości zamiast odwołania do wartości. Zasadniczo spowoduje to, że dzwoniący wykona kopię zamiast ruchu, alemove_if_noexcept
nie robi kopii. W przeciwnym razie świetne wyjaśnienie.noexcept
. Tak więc „nigdy” nie jest prawdą.std::vector
jest napisane, aby zmusić kompilator do kompilacji innego kodu. Nie chodzi o to, że kompilator coś wykrywa; chodzi o wykrywanie czegoś przez kod użytkownika.W Bjarne „s słowa ( C ++ Programming Language, 4. wydanie , strona 366):
źródło
noexcept
każdym razem, chyba że wyraźnie chcę zająć się wyjątkami. Bądźmy szczerzy, większość wyjątków jest tak nieprawdopodobna i / lub tak śmiertelna, że ratowanie jest prawie nieuzasadnione lub możliwe. Np. W przytoczonym przykładzie, jeśli alokacja się nie powiedzie, aplikacja nie będzie mogła dalej działać poprawnie.noexcept
jest trudne, ponieważ stanowi część interfejsu funkcji. W szczególności, jeśli piszesz bibliotekę, kod klienta może zależeć odnoexcept
właściwości. Późniejsza zmiana może być trudna, ponieważ możesz uszkodzić istniejący kod. Może to być mniej istotne, gdy implementujesz kod, który jest używany tylko przez twoją aplikację.Jeśli masz funkcję, która nie może rzucać, zadaj sobie pytanie, czy będzie
noexcept
chciał zostać, czy ograniczy przyszłe wdrożenia? Na przykład możesz chcieć wprowadzić sprawdzanie błędów niedozwolonych argumentów poprzez zgłaszanie wyjątków (np. Dla testów jednostkowych) lub możesz polegać na innym kodzie biblioteki, który mógłby zmienić specyfikację wyjątku. W takim przypadku bezpieczniej jest być konserwatywnym i pominąćnoexcept
.Z drugiej strony, jeśli masz pewność, że funkcja nigdy nie powinna wyrzucać i jest prawdą, że jest częścią specyfikacji, powinieneś ją zadeklarować
noexcept
. Należy jednak pamiętać, że kompilator nie będzie w stanie wykryć naruszeń wnoexcept
przypadku zmiany implementacji.Istnieją cztery klasy funkcji, na które powinieneś się skoncentrować, ponieważ prawdopodobnie będą one miały największy wpływ:
noexcept(true)
chyba że je zrobisznoexcept(false)
)Funkcje te powinny zasadniczo być
noexcept
i najprawdopodobniej implementacje bibliotek mogą korzystać z tejnoexcept
właściwości. Na przykładstd::vector
można użyć operacji ruchu bez rzucania bez poświęcania silnych gwarancji wyjątków. W przeciwnym razie będzie musiał wrócić do kopiowania elementów (tak jak w C ++ 98).Tego rodzaju optymalizacja odbywa się na poziomie algorytmicznym i nie polega na optymalizacjach kompilatora. Może to mieć znaczący wpływ, szczególnie jeśli kopiowanie elementów jest kosztowne.
Zaletą jest
noexcept
brak specyfikacji wyjątków lubthrow()
to, że standard pozwala kompilatorom na większą swobodę podczas rozwijania stosu. Nawet w tymthrow()
przypadku kompilator musi całkowicie rozwinąć stos (i musi to zrobić w dokładnie odwrotnej kolejności konstrukcji obiektów).Z
noexcept
drugiej strony w przypadku nie jest to wymagane. Nie ma wymogu rozwijania stosu (ale kompilator nadal może to robić). Ta swoboda pozwala na dalszą optymalizację kodu, ponieważ obniża narzut związany z ciągłym rozwijaniem stosu.Powiązane pytanie dotyczące noexcept, rozwijania stosu i wydajności zawiera więcej szczegółów na temat narzutu, gdy wymagane jest rozwijanie stosu.
Polecam także książkę Scotta Meyersa „Effective Modern C ++”, „Punkt 14: Deklarowanie funkcji, z wyjątkiem przypadków, gdy nie będą wydawać wyjątków” do dalszego czytania.
źródło
throws
słowo kluczowe zamiastnoexcept
ujemnego. Po prostu nie mogę uzyskać niektórych opcji projektowania w C ++ ...noexcept
ponieważthrow
został już zajęty. Po prostuthrow
można go używać prawie tak, jak wspomniałeś, z tym wyjątkiem, że spartaczyli jego projekt, dzięki czemu stał się prawie bezużyteczny - nawet szkodliwy. Ale utknęliśmy z tym teraz, ponieważ usunięcie go byłoby przełomową zmianą, przynoszącą niewielkie korzyści. Taknoexcept
jest w zasadziethrow_v2
.throw
nie jest przydatne?throw()
wyjątku nie dał takiej samej gwarancji jaknothrow
?Kiedy mówisz „Wiem, że [oni] nigdy nie rzucą”, masz na myśli, analizując implementację funkcji, wiesz, że funkcja nie będzie rzucała. Myślę, że to podejście jest odwrócone.
Lepiej jest rozważenie, czy funkcja może rzucać wyjątki być częścią projektu funkcji: równie ważne jak listy argumentów i czy metoda jest mutator (...
const
). Deklaracja, że „ta funkcja nigdy nie zgłasza wyjątków” stanowi ograniczenie dla implementacji. Pominięcie go nie oznacza, że funkcja może zgłaszać wyjątki; oznacza to, że bieżąca wersja funkcji i wszystkie przyszłe wersje mogą zgłaszać wyjątki. Jest to ograniczenie, które utrudnia wdrożenie. Ale niektóre metody muszą mieć ograniczenie, aby były praktycznie przydatne; co najważniejsze, aby można je było wywoływać z destruktorów, ale także w celu implementacji kodu „wycofywania” w metodach zapewniających silną gwarancję wyjątku.źródło