Jaki jest sens niewiedzy?

189

[dcl.attr.noreturn] podaje następujący przykład:

[[ noreturn ]] void f() {
    throw "error";
    // OK
}

ale nie rozumiem, o co chodzi [[noreturn]], ponieważ typ zwracanej funkcji już jest void.

Jaki jest sens tego noreturnatrybutu? Jak ma być używany?

BЈовић
źródło
1
Co jest takiego ważnego w tego rodzaju funkcjach (które najprawdopodobniej wystąpią po uruchomieniu programu), które zasługują na taką uwagę? Czy nie jest to sytuacja łatwo wykrywalna?
user666412,
1
@MrLister PO łączący pojęcia „zwrotu” i „wartości zwrotu”. Biorąc pod uwagę, że prawie zawsze są one używane w tandemie, myślę, że zamieszanie jest uzasadnione.
Slipp D. Thompson

Odpowiedzi:

209

Atrybut noreturn powinien być używany dla funkcji, które nie wracają do programu wywołującego. Nie oznacza to nieważnych funkcji (które zwracają się do wywołującego - po prostu nie zwracają wartości), ale funkcje, w których przepływ sterowania nie powróci do funkcji wywołującej po zakończeniu funkcji (np. Funkcje, które wychodzą z aplikacji, zapętlaj wiecznie lub zgłaszaj wyjątki jak w twoim przykładzie).

Może to być wykorzystane przez kompilatory do optymalizacji i generowania lepszych ostrzeżeń. Na przykład, jeśli fma atrybut noreturn, kompilator może ostrzegać cię g()przed martwym kodem podczas pisania f(); g();. Podobnie kompilator będzie wiedział, aby nie ostrzegać o brakujących instrukcjach zwrotnych po wywołaniach do f().

sepp2k
źródło
5
Co z funkcją taką jak execveta nie powinna powrócić, ale mogłaby ? Czy powinien mieć atrybut noreturn ?
Kalrish
22
Nie, nie powinno - jeśli istnieje możliwość, aby przepływ kontroli powrócił do dzwoniącego, nie może mieć noreturnatrybutu. noreturnmoże być użyty tylko wtedy, gdy twoja funkcja ma gwarancję zrobienia czegoś, co kończy działanie programu, zanim przepływ sterowania może powrócić do programu wywołującego - na przykład dlatego, że wywołujesz exit (), abort (), assert (0) itp.
RavuAlHemio
6
@ SlippD.Thompson Jeśli wywołanie funkcji powrotu do środka jest opakowane w try-block, każdy kod z bloku catch będzie ponownie liczony jako osiągalny.
sepp2k
2
@ sepp2k Cool. Więc powrót nie jest niemożliwy, po prostu nienormalny. To się przydaje. Twoje zdrowie.
Slipp D. Thompson
7
@ SlippD.Thompson nie, nie można wrócić. Zgłoszenie wyjątku nie powraca, więc jeśli każda ścieżka rzuci, to jest noreturn. Obsługa tego wyjątku nie jest tym samym, co jego zwrócenie. Jakikolwiek kod trypo wywołaniu jest nadal nieosiągalny, a jeśli nie, voidwówczas żadne przypisanie lub użycie wartości zwracanej nie nastąpi.
Jon Hanna
63

noreturnnie mówi kompilatorowi, że funkcja nie zwraca żadnej wartości. Mówi kompilatorowi, że przepływ kontrolny nie powróci do dzwoniącego . Umożliwia to kompilatorowi dokonywanie różnych optymalizacji - nie musi zapisywać i przywracać żadnego niestabilnego stanu wokół połączenia, może kodować martwy kod, eliminując kod, który w przeciwnym razie byłby zgodny z połączeniem itp.

Stephen Canon
źródło
29

Oznacza to, że funkcja się nie zakończy. Przepływ sterowania nigdy nie uderzy w instrukcję po wywołaniu f():

void g() {
   f();
   // unreachable:
   std::cout << "No! That's impossible" << std::endl;
}

Informacje mogą być wykorzystywane przez kompilator / optymalizator na różne sposoby. Kompilator może dodać ostrzeżenie, że powyższy kod jest nieosiągalny i może modyfikować rzeczywisty kod g()na różne sposoby, na przykład w celu obsługi kontynuacji.

David Rodríguez - dribeas
źródło
3
gcc / clang nie dają ostrzeżeń
TemplateRex
4
@TemplateRex: Skompiluj się, -Wno-returna otrzymasz ostrzeżenie. Prawdopodobnie nie ten, którego się spodziewałeś, ale prawdopodobnie wystarczy powiedzieć, że kompilator ma wiedzę na temat tego, co [[noreturn]]jest i może z tego skorzystać. (Jestem trochę zaskoczony, że -Wunreachable-codenie udało mi się ...)
David Rodríguez - dribeas
3
@TemplateRex: Przepraszamy -Wmissing-noreturn, ostrzeżenie oznacza, że ​​analiza przepływu ustaliła, że std::coutnie można go osiągnąć. Nie mam pod ręką wystarczająco nowej gcc, aby spojrzeć na wygenerowany zestaw, ale nie byłbym zaskoczony, gdyby połączenie operator<<zostało przerwane
David Rodríguez - dribeas
1
Oto zrzut zestawu (-S -o - flagi w coliru), rzeczywiście upuszcza kod „nieosiągalny”. Co ciekawe, wystarczy -O1już usunąć ten nieosiągalny kod bez [[noreturn]]podpowiedzi.
TemplateRex,
2
@TemplateRex: Cały kod jest w tej samej jednostce tłumaczeniowej i jest widoczny, więc kompilator może wywnioskować [[noreturn]]z kodu. Gdyby ta jednostka tłumacząca miała tylko deklarację funkcji zdefiniowanej gdzie indziej, kompilator nie byłby w stanie usunąć tego kodu, ponieważ nie wie, że funkcja nie zwraca. Właśnie tam atrybut powinien pomóc kompilatorowi.
David Rodríguez - dribeas
17

Poprzednie odpowiedzi poprawnie wyjaśniały, czym jest noreturn, ale nie dlaczego . Nie sądzę, aby komentarze dotyczące „optymalizacji” były głównym celem: funkcje, które nie zwracają się, są rzadkie i zwykle nie wymagają optymalizacji. Myślę raczej, że główną racją bytu noreturn jest unikanie fałszywie pozytywnych ostrzeżeń. Rozważmy na przykład ten kod:

int f(bool b){
    if (b) {
        return 7;
    } else {
        abort();
    }
 }

Gdyby abort () nie został oznaczony jako „noreturn”, kompilator mógłby ostrzec o tym, że ten kod ma ścieżkę, w której f nie zwraca liczby całkowitej zgodnie z oczekiwaniami. Ale ponieważ abort () jest oznaczony jako brak powrotu, wie, że kod jest poprawny.

Nadav Har'El
źródło
Wszystkie pozostałe wymienione przykłady używają funkcji void - jak to działa, gdy masz zarówno dyrektywę [[brak zwrotu]], jak i typ zwrotu nieważny? Czy dyrektywa [[no return]] wchodzi w grę dopiero wtedy, gdy kompilator przygotowuje się, aby ostrzec przed możliwością powrotu i zignorować ostrzeżenie? Na przykład, czy kompilator mówi: „Dobra, oto funkcja nie-void”. * kontynuuje kompilację * „O cholera, ten kod może nie zwrócić! Czy powinienem ostrzec użytkownika? *” Nieważne, widzę dyrektywę o braku zwrotu. Kontynuuj ”
Raleigh L.
2
Funkcja noreturn w moim przykładzie to nie f (), to abort (). Nie ma sensu oznaczanie non-void funkcji powrotnej. Funkcja, która czasami zwraca wartość, a czasami zwraca (dobrym przykładem jest execve ()), nie może być oznaczona jako niezwrócona.
Nadav Har'El
1
niejawny upadek to kolejny taki przykład: stackoverflow.com/questions/45129741/...
Ciro Santilli 郝海东 冠状 病 六四 事件 法轮功
11

Typ piszący teoretycznie voidto, co nazywa się w innych językach unitlub top. Jego logicznym odpowiednikiem jest Prawda . Każda wartość może być legalnie przypisana void(każdy typ jest podtypem void). Pomyśl o tym jako o zestawie „wszechświata”; nie ma wspólnych operacji dla wszystkich wartości na świecie, więc nie ma prawidłowych operacji na wartości typu void. Mówiąc inaczej, mówiąc, że coś należy do zestawu wszechświata, nie daje żadnych informacji - już to wiesz. Tak więc dźwięk jest następujący:

(void)5;
(void)foo(17); // whatever foo(17) does

Ale poniższe zadanie nie jest:

void raise();
void f(int y) {
    int x = y!=0 ? 100/y : raise(); // raise() returns void, so what should x be?
    cout << x << endl;
}

[[noreturn]]Z drugiej strony, nazywa się czasami empty, Nothing, Bottomlub Boti jest logicznym odpowiednikiem fałszywy . Nie ma w ogóle żadnych wartości, a wyrażenie tego typu można rzutować na dowolny typ (tj. Jest podtypem). To jest pusty zestaw. Zauważ, że jeśli ktoś powie ci „wartość wyrażenia foo () należy do pustego zestawu”, jest to bardzo pouczające - mówi ci, że to wyrażenie nigdy nie zakończy normalnego wykonania; przerwie, rzuci lub zawiesi się. Jest to dokładne przeciwieństwo void.

Poniższe nie ma więc sensu (pseudo-C ++, ponieważ noreturnnie jest to typ pierwszej klasy C ++)

void foo();
(noreturn)5; // obviously a lie; the expression 5 does "return"
(noreturn)foo(); // foo() returns void, and therefore returns

Ale poniższe przypisanie jest całkowicie uzasadnione, ponieważ throwkompilator rozumie, że nie zwraca:

void f(int y) {
    int x = y!=0 ? 100/y : throw exception();
    cout << x << endl;
}

W idealnym świecie możesz użyć noreturnjako wartości zwracanej dla raise()powyższej funkcji :

noreturn raise() { throw exception(); }
...
int x = y!=0 ? 100/y : raise();

Niestety C ++ na to nie pozwala, prawdopodobnie ze względów praktycznych. Zamiast tego daje możliwość użycia [[ noreturn ]]atrybutu, który pomaga kierować optymalizacjami kompilatora i ostrzeżeniami.

Elazar
źródło
5
Nic nie może być oddane do voidi voidnigdy nie ocenia się trueczy falsealbo cokolwiek innego.
Jaśniejsze
5
Kiedy mówię true, nie mam na myśli „wartości truetypu bool”, ale logiczny sens, patrz korespondencja Curry-Howarda
Elazar
8
Abstrakcyjna teoria typów, która nie pasuje do systemu określonego języka, nie ma znaczenia przy omawianiu systemu typów tego języka. Pytanie, o którym mowa :-), dotyczy C ++, a nie teorii typów.
Jaśniejsze
8
(void)true;jest całkowicie poprawny, jak sugeruje odpowiedź. void(true)jest czymś zupełnie innym, składniowo. Jest to próba utworzenia nowego obiektu typu voidpoprzez wywołanie konstruktora truejako argumentu; nie udaje się to między innymi dlatego, że voidnie jest to pierwsza klasa.
Elazar
2
Tak jest. Jestem przyzwyczajony do używania rzutowania typu (wartości), a nie rzutowania w stylu c.
Jaśniejsze