Schlüsselwort in die Signatur der Funktion einfügen

199

Was ist der technische Grund, warum es als schlechte Praxis angesehen wird, das throwSchlüsselwort C ++ in einer Funktionssignatur zu verwenden?

bool some_func() throw(myExc)
{
  ...
  if (problem_occurred) 
  {
    throw myExc("problem occurred");
  }
  ...
}
Konstantin
quelle
Siehe diese aktuelle verwandte Frage: stackoverflow.com/questions/1037575/…
laalto
1
Ändert noexceptsich etwas?
Aaron McDaid
12
Es ist nichts Falsches daran, Meinungen zu etwas Programmierbezogenem zu haben. Dieses Abschlusskriterium ist zumindest für diese Frage schlecht. Es ist eine interessante Frage mit interessanten Antworten.
Anders Lindén
Es ist zu beachten, dass Ausnahmespezifikationen seit C ++ 11 veraltet sind.
François Andrieux

Antworten:

127

Nein, dies wird nicht als bewährte Methode angesehen. Im Gegenteil, es wird allgemein als schlechte Idee angesehen.

http://www.gotw.ca/publications/mill22.htm geht viel detaillierter auf das Warum ein, aber das Problem ist teilweise, dass der Compiler dies nicht erzwingen kann, so dass es zur Laufzeit überprüft werden muss, was normalerweise der Fall ist unerwünscht. Und es wird auf keinen Fall gut unterstützt. (MSVC ignoriert Ausnahmespezifikationen mit Ausnahme von throw (), die es als Garantie dafür interpretiert, dass keine Ausnahme ausgelöst wird.

jalf
quelle
25
Ja. Es gibt bessere Möglichkeiten, Ihrem Code Leerzeichen hinzuzufügen als throw (myEx).
Assaf Lavie
4
Ja, Leute, die nur die Ausnahmespezifikationen entdecken, gehen oft davon aus, dass sie wie in Java arbeiten, wo der Compiler sie durchsetzen kann. In C ++ wird das nicht passieren, was sie viel weniger nützlich macht.
Jalf
7
Aber wie steht es dann mit dem dokumentarischen Zweck? Außerdem wird Ihnen mitgeteilt, welche Ausnahmen Sie niemals abfangen werden, selbst wenn Sie es versuchen.
Anders Lindén
1
@ AndersLindén welchen dokumentativen Zweck? Wenn Sie nur das Verhalten Ihres Codes dokumentieren möchten, setzen Sie einfach einen Kommentar darüber. Ich bin mir nicht sicher, was du mit dem zweiten Teil meinst. Ausnahmen, die Sie niemals fangen werden, selbst wenn Sie es versuchen?
Jalf
4
Ich denke, Code ist mächtig, wenn es darum geht, Ihren Code zu dokumentieren. (Kommentare können lügen). Der "dokumentative" Effekt, auf den ich mich beziehe, besteht darin, dass Sie sicher wissen, welche Ausnahmen Sie fangen können und welche nicht.
Anders Lindén
57

Jalf ist bereits damit verbunden, aber das GOTW bringt es ganz gut auf den Punkt , warum Ausnahmespezifikationen nicht so nützlich sind, wie man hoffen könnte:

int Gunc() throw();    // will throw nothing (?)
int Hunc() throw(A,B); // can only throw A or B (?)

Sind die Kommentare korrekt? Nicht ganz. Gunc()kann in der Tat etwas werfen, und Hunc()kann gut etwas anderes als A oder B werfen! Der Compiler garantiert nur, dass er sie sinnlos schlägt, wenn sie es tun ... oh, und dass Sie Ihr Programm die meiste Zeit auch sinnlos schlagen.

Das ist genau das, worauf es ankommt. Sie werden wahrscheinlich nur einen Anruf erhalten terminate()und Ihr Programm stirbt an einem schnellen, aber schmerzhaften Tod.

Die Schlussfolgerung der GOTW lautet:

Hier ist der beste Rat, den wir als Community bis heute gelernt haben:

  • Moral Nr. 1: Schreiben Sie niemals eine Ausnahmespezifikation.
  • Moral Nr. 2: Außer vielleicht einer leeren, aber wenn ich du wäre, würde ich auch das vermeiden.
etw
quelle
1
Ich bin mir nicht sicher, warum ich eine Ausnahme auslösen und sie nicht erwähnen kann. Selbst wenn es von einer anderen Funktion ausgelöst wird, weiß ich, welche Ausnahmen ausgelöst werden könnten. Der einzige Grund, den ich sehen kann, ist, dass es langweilig ist.
MasterMastic
3
@ Ken: Der Punkt ist, dass das Schreiben von Ausnahmespezifikationen meist negative Konsequenzen hat. Der einzige positive Effekt ist, dass es dem Programmierer zeigt, welche Ausnahmen auftreten können, aber da es vom Compiler nicht in angemessener Weise überprüft wird, ist es fehleranfällig und daher nicht viel wert.
etw
1
Oh okay, danke für die Antwort. Ich denke, dafür ist Dokumentation gedacht.
MasterMastic
2
Nicht richtig. Die Ausnahmespezifikation sollte geschrieben werden, aber die Idee ist zu kommunizieren, welche Fehler der Anrufer zu fangen versuchen sollte.
HelloWorld
1
Genau wie @StudentT sagt: Es liegt in der Verantwortung der Funktion, sicherzustellen, dass keine weiteren Ausnahmen mehr ausgelöst werden. In diesem Fall wird das Programm ordnungsgemäß beendet. Wurf zu deklarieren bedeutet, dass es nicht meine Verantwortung ist, mit dieser Situation umzugehen, und der Anrufer sollte über genügend Informationen verfügen, um dies zu tun. Wenn keine Ausnahmen deklariert werden, können sie überall auftreten und überall behandelt werden. Das ist sicherlich ein Anti-OOP-Chaos. Es ist ein Designfehler, Ausnahmen an falschen Stellen abzufangen. Ich würde empfehlen, nicht leer zu werfen, da Ausnahmen außergewöhnlich sind und die meisten Funktionen sowieso leer werfen sollten.
Jan Turoň
30

Um allen anderen Antworten auf diese Frage einen Mehrwert zu verleihen, sollte man einige Minuten in die Frage investieren: Was ist die Ausgabe des folgenden Codes?

#include <iostream>
void throw_exception() throw(const char *)
{
    throw 10;
}
void my_unexpected(){
    std::cout << "well - this was unexpected" << std::endl;
}
int main(int argc, char **argv){
    std::set_unexpected(my_unexpected);
    try{
        throw_exception();
    }catch(int x){
        std::cout << "catch int: " << x << std::endl;
    }catch(...){
        std::cout << "catch ..." << std::endl;
    }
}

Antwort: Wie hier erwähnt , wird das Programm std::terminate()aufgerufen und somit wird keiner der Ausnahmebehandler aufgerufen.

Details: Die erste my_unexpected()Funktion wird aufgerufen, aber da sie am Ende keinen passenden Ausnahmetyp für den throw_exception()Funktionsprototyp erneut auslöst, std::terminate()wird sie aufgerufen. Die vollständige Ausgabe sieht also folgendermaßen aus:

Benutzer @ Benutzer: ~ / tmp $ g ++ -o außer Test außer Test.cpp
Benutzer @ Benutzer: ~ / tmp $ ./except.test
gut - dies war ein unerwarteter
Abschluss, der aufgerufen wurde, nachdem eine Instanz von 'int'
Aborted (core) ausgelöst wurde abgeladen)

John Doe
quelle
12

Der einzige praktische Effekt des Wurfspezifizierers besteht darin myExc, std::unexpecteddass aufgerufen wird, wenn etwas anderes als von Ihrer Funktion ausgelöst wird (anstelle des normalen nicht behandelten Ausnahmemechanismus).

Um die Art von Ausnahmen zu dokumentieren, die eine Funktion auslösen kann, gehe ich normalerweise folgendermaßen vor:

bool
some_func() /* throw (myExc) */ {
}
Paolo Tedesco
quelle
5
Es ist auch nützlich zu beachten, dass ein Aufruf von std :: unerwartet () normalerweise zu einem Aufruf von std :: terminate () und dem abrupten Ende Ihres Programms führt.
etw
1
- und dass MSVC dieses Verhalten meines Wissens zumindest nicht implementiert.
Jalf
9

Als der Sprache Wurfspezifikationen hinzugefügt wurden, war dies mit den besten Absichten geschehen, aber die Praxis hat einen praktischeren Ansatz bestätigt.

In C ++ lautet meine allgemeine Faustregel, nur Wurfspezifikationen zu verwenden, um anzuzeigen, dass eine Methode nicht werfen kann. Dies ist eine starke Garantie. Ansonsten gehe davon aus, dass es alles werfen könnte.

Greg D.
quelle
9

Nun, während ich über diese Wurfspezifikation googelte, habe ich mir diesen Artikel angesehen: - ( http://blogs.msdn.com/b/larryosterman/archive/2006/03/22/558390.aspx )

Ich reproduziere auch hier einen Teil davon, damit er in Zukunft verwendet werden kann, unabhängig davon, ob der obige Link funktioniert oder nicht.

   class MyClass
   {
    size_t CalculateFoo()
    {
        :
        :
    };
    size_t MethodThatCannotThrow() throw()
    {
        return 100;
    };
    void ExampleMethod()
    {
        size_t foo, bar;
        try
        {
            foo = CalculateFoo();
            bar = foo * 100;
            MethodThatCannotThrow();
            printf("bar is %d", bar);
        }
        catch (...)
        {
        }
    }
};

Wenn der Compiler dies mit dem Attribut "throw ()" sieht, kann der Compiler die Variable "bar" vollständig optimieren, da er weiß, dass eine Ausnahme von MethodThatCannotThrow () nicht ausgelöst werden kann. Ohne das Attribut throw () muss der Compiler die Variable "bar" erstellen. Wenn MethodThatCannotThrow eine Ausnahme auslöst, kann / wird der Ausnahmebehandler vom Wert der Balkenvariablen abhängen.

Darüber hinaus können (und werden) Quellcode-Analysetools wie prefast die Funktion throw () verwenden, um ihre Fehlererkennungsfunktionen zu verbessern. Wenn Sie beispielsweise einen Versuch / Fang ausführen und alle von Ihnen aufgerufenen Funktionen als throw () markiert sind, Sie brauchen das try / catch nicht (ja, dies hat ein Problem, wenn Sie später eine Funktion aufrufen, die auslösen könnte).

Pratik Singhal
quelle
3

Eine No-Throw-Spezifikation für eine Inline-Funktion, die nur eine Mitgliedsvariable zurückgibt und möglicherweise keine Ausnahmen auslösen kann, kann von einigen Compilern verwendet werden, um Pessimierungen durchzuführen (ein erfundenes Wort für das Gegenteil von Optimierungen), die sich nachteilig auf die Leistung auswirken können. Dies ist in der Boost-Literatur beschrieben: Ausnahmespezifikation

Bei einigen Compilern kann eine No-Throw-Spezifikation für Nicht-Inline-Funktionen von Vorteil sein, wenn die richtigen Optimierungen vorgenommen werden und die Verwendung dieser Funktion die Leistung in einer Weise beeinflusst, die dies rechtfertigt.

Für mich klingt es so, als ob die Verwendung ein Aufruf eines sehr kritischen Auges im Rahmen einer Leistungsoptimierung ist, möglicherweise mithilfe von Profiling-Tools.

Ein Zitat aus dem obigen Link für diejenigen, die es eilig haben (enthält ein Beispiel für schlechte unbeabsichtigte Auswirkungen der Angabe eines Wurfs auf eine Inline-Funktion eines naiven Compilers):

Begründung der Ausnahmespezifikation

Ausnahmespezifikationen [ISO 15.4] werden manchmal codiert, um anzugeben, welche Ausnahmen ausgelöst werden können, oder weil der Programmierer hofft, dass sie die Leistung verbessern. Betrachten Sie jedoch das folgende Element anhand eines intelligenten Zeigers:

T & operator * () const throw () {return * ptr; }}

Diese Funktion ruft keine anderen Funktionen auf. Es werden nur grundlegende Datentypen wie Zeiger bearbeitet. Daher kann niemals ein Laufzeitverhalten der Ausnahmespezifikation aufgerufen werden. Die Funktion ist vollständig dem Compiler zugänglich. In der Tat wird es als Inline deklariert. Daher kann ein intelligenter Compiler leicht ableiten, dass die Funktionen keine Ausnahmen auslösen können, und dieselben Optimierungen vornehmen, die er basierend auf der leeren Ausnahmespezifikation vorgenommen hätte. Ein "dummer" Compiler kann jedoch alle Arten von Pessimierungen vornehmen.

Beispielsweise deaktivieren einige Compiler das Inlining, wenn eine Ausnahmespezifikation vorliegt. Einige Compiler fügen Try / Catch-Blöcke hinzu. Solche Pessimierungen können eine Leistungskatastrophe sein, die den Code in praktischen Anwendungen unbrauchbar macht.

Obwohl eine Ausnahmespezifikation zunächst ansprechend ist, hat sie tendenziell Konsequenzen, deren Verständnis sehr sorgfältige Überlegungen erfordert. Das größte Problem bei Ausnahmespezifikationen besteht darin, dass Programmierer sie so verwenden, als hätten sie den Effekt, den der Programmierer haben möchte, anstatt den Effekt, den sie tatsächlich haben.

Eine Nicht-Inline-Funktion ist die einzige Stelle, an der eine Ausnahmespezifikation "wirft nichts" bei einigen Compilern einige Vorteile haben kann.

Pablo Adames
quelle
1
"Das größte Problem bei Ausnahmespezifikationen besteht darin, dass Programmierer sie so verwenden, als hätten sie den Effekt, den der Programmierer haben möchte, anstatt den Effekt, den sie tatsächlich haben." Dies ist der Hauptgrund für Fehler bei der Kommunikation mit Maschinen oder Menschen: der Unterschied zwischen dem, was wir sagen und dem, was wir meinen.
Thagomizer