Lancer un mot-clé dans la signature de la fonction

200

Quelle est la raison technique pour laquelle il est considéré comme une mauvaise pratique d'utiliser le throwmot clé C ++ dans une signature de fonction?

bool some_func() throw(myExc)
{
  ...
  if (problem_occurred) 
  {
    throw myExc("problem occurred");
  }
  ...
}
Konstantin
la source
Voir cette récente question connexe: stackoverflow.com/questions/1037575/…
laalto
1
Cela noexceptchange- t-il quelque chose?
Aaron McDaid
12
Il n'y a rien de mal à avoir des opinions sur quelque chose de lié à la programmation. Ce critère de clôture est mauvais, du moins pour cette question. C'est une question intéressante avec des réponses intéressantes.
Anders Lindén
Il convient de noter que les spécifications d'exception sont obsolètes depuis C ++ 11.
François Andrieux

Réponses:

127

Non, ce n'est pas considéré comme une bonne pratique. Au contraire, il est généralement considéré comme une mauvaise idée.

http://www.gotw.ca/publications/mill22.htm explique en détail pourquoi, mais le problème est en partie que le compilateur n'est pas en mesure de le faire appliquer, il doit donc être vérifié au moment de l'exécution, ce qui est généralement indésirable. Et ce n'est pas bien supporté en tout cas. (MSVC ignore les spécifications d'exception, à l'exception de throw (), qu'il interprète comme une garantie qu'aucune exception ne sera levée.

jalf
la source
25
Oui. Il existe de meilleures façons d'ajouter des espaces à votre code que throw (myEx).
Assaf Lavie
4
oui, les gens qui viennent de découvrir les spécifications d'exception supposent souvent qu'ils fonctionnent comme en Java, où le compilateur est capable de les appliquer. En C ++, cela ne se produira pas, ce qui les rend beaucoup moins utiles.
jalf
7
Mais qu'en est-il du but documentaire? De plus, on vous dira quelles exceptions vous ne rattraperez jamais même si vous essayez.
Anders Lindén
1
@ AndersLindén quel but documentaire? Si vous voulez simplement documenter le comportement de votre code, mettez simplement un commentaire dessus. Je ne sais pas ce que vous voulez dire avec la deuxième partie. Des exceptions que vous n'attraperez jamais même si vous essayez?
1er
4
Je pense que le code est puissant lorsqu'il s'agit de documenter votre code. (les commentaires peuvent mentir). L'effet "documentaire" auquel je fais référence est que vous saurez avec certitude quelles exceptions vous pouvez attraper et d'autres ne peuvent pas être.
Anders Lindén
57

Jalf y est déjà lié, mais le GOTW explique très bien pourquoi les spécifications d'exception ne sont pas aussi utiles qu'on pourrait l'espérer:

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

Les commentaires sont-ils corrects? Pas assez. Gunc()peut en effet lancer quelque chose, et Hunc()peut bien lancer autre chose que A ou B! Le compilateur garantit juste de les battre insensés s'ils le font… oh, et de battre votre programme aussi insensé, la plupart du temps.

C'est exactement ce à quoi cela se résume, vous finirez probablement avec un appel à terminate() et votre programme mourra d'une mort rapide mais douloureuse.

La conclusion de GOTW est:

Voici donc ce qui semble être le meilleur conseil que nous avons appris en tant que communauté à ce jour:

  • Moral # 1: N'écrivez jamais de spécification d'exception.
  • Moral # 2: Sauf peut-être un vide, mais si j'étais vous, j'éviterais même cela.
qch
la source
1
Je ne sais pas pourquoi je lèverais une exception et ne pourrais pas le mentionner. Même si elle est levée par une autre fonction, je sais quelles exceptions peuvent être levées. La seule raison pour laquelle je peux voir, c'est parce que c'est fastidieux.
MasterMastic
3
@Ken: Le fait est que l'écriture de spécifications d'exception a principalement des conséquences négatives. Le seul effet positif est qu'il montre au programmeur quelles exceptions peuvent se produire, mais comme il n'est pas vérifié par le compilateur de manière raisonnable, il est sujet à des erreurs et ne vaut donc pas grand-chose.
STH
1
Oh d'accord, merci d'avoir répondu. Je suppose que c'est à ça que sert la documentation.
MasterMastic
2
Pas correcte. La spécification d'exception doit être écrite, mais l'idée est de communiquer les erreurs que l'appelant doit essayer d'attraper.
HelloWorld
1
Tout comme @StudentT le dit: il est de la responsabilité de la fonction de garantir de ne plus lever d'autres exceptions. S'ils le font, le programme se termine comme il se doit. Déclarer lancer signifie qu'il n'est pas de ma responsabilité de gérer cette situation et que l'appelant doit avoir suffisamment d'informations pour le faire. Ne pas déclarer d'exceptions signifie qu'elles peuvent se produire n'importe où et qu'elles peuvent être gérées n'importe où. C'est certainement un gâchis anti-OOP. C'est un échec de conception pour attraper des exceptions aux mauvais endroits. Je recommanderais de ne pas jeter à vide, car les exceptions sont exceptionnelles et la plupart des fonctions devraient de toute façon jeter à vide.
Jan Turoň
30

Pour ajouter un peu plus de valeur à toutes les autres réponses à cette question, il faut investir quelques minutes dans la question: Quelle est la sortie du code suivant?

#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;
    }
}

Réponse: Comme indiqué ici , le programme appelle std::terminate()et donc aucun des gestionnaires d'exceptions ne sera appelé.

Détails: La première my_unexpected()fonction est appelée, mais comme elle ne rejette pas de type d'exception correspondant pour le throw_exception()prototype de fonction, elle std::terminate()est finalement appelée. Donc, la sortie complète ressemble à ceci:

user @ user: ~ / tmp $ g ++ -o except.test except.test.cpp
user @ user: ~ / tmp $ ./except.test
bien - c'était une
terminaison inattendue appelée après avoir lancé une instance de 'int'
Aborted (core vidé)

John Doe
la source
12

Le seul effet pratique du spécificateur throw est que si quelque chose de différent de myExcest levé par votre fonction, std::unexpectedil sera appelé (au lieu du mécanisme d'exception normal non géré).

Pour documenter le type d'exceptions qu'une fonction peut lever, je fais généralement ceci:

bool
some_func() /* throw (myExc) */ {
}
Paolo Tedesco
la source
5
Il est également utile de noter qu'un appel à std :: inattendu () entraîne généralement un appel à std :: terminate () et la fin brutale de votre programme.
STH
1
- et que MSVC au moins n'implémente pas ce comportement à ma connaissance.
jalf
9

Lorsque les spécifications de projection ont été ajoutées au langage, c'était avec les meilleures intentions, mais la pratique a confirmé une approche plus pratique.

Avec C ++, ma règle générale est de n'utiliser que les spécifications de projection pour indiquer qu'une méthode ne peut pas lancer. Il s'agit d'une garantie solide. Sinon, supposez qu'il puisse lancer n'importe quoi.

Greg D
la source
9

Eh bien, lors de la recherche sur cette spécification de lancement, j'ai lu cet article: - ( http://blogs.msdn.com/b/larryosterman/archive/2006/03/22/558390.aspx )

J'en reproduis une partie ici également, afin qu'il puisse être utilisé à l'avenir indépendamment du fait que le lien ci-dessus fonctionne ou non.

   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 (...)
        {
        }
    }
};

Lorsque le compilateur voit cela, avec l'attribut "throw ()", le compilateur peut complètement optimiser la variable "bar", car il sait qu'il n'y a aucun moyen de lever une exception à partir de MethodThatCannotThrow (). Sans l'attribut throw (), le compilateur doit créer la variable "bar", car si MethodThatCannotThrow lève une exception, le gestionnaire d'exceptions peut / dépendra de la valeur de la variable bar.

De plus, les outils d'analyse de code source comme prefast peuvent (et utiliseront) l'annotation throw () pour améliorer leurs capacités de détection d'erreurs - par exemple, si vous avez un try / catch et que toutes les fonctions que vous appelez sont marquées comme throw (), vous n'avez pas besoin de try / catch (oui, cela a un problème si vous appelez plus tard une fonction qui pourrait lancer).

Pratik Singhal
la source
3

Une spécification de non-projection sur une fonction en ligne qui ne renvoie qu'une variable membre et qui ne pourrait pas lever des exceptions peut être utilisée par certains compilateurs pour effectuer des pessimisations (un mot inventé pour l'opposé des optimisations) qui peuvent avoir un effet néfaste sur les performances. Ceci est décrit dans la littérature Boost: Spécification d'exception

Avec certains compilateurs, une spécification de non-projection sur les fonctions non en ligne peut être bénéfique si les optimisations correctes sont faites et que l'utilisation de cette fonction a un impact sur les performances d'une manière qui la justifie.

Pour moi, il semble que l'utilisation ou non soit un appel lancé par un œil très critique dans le cadre d'un effort d'optimisation des performances, peut-être à l'aide d'outils de profilage.

Une citation du lien ci-dessus pour ceux qui sont pressés (contient un exemple de mauvais effets involontaires de spécification de lancer sur une fonction en ligne à partir d'un compilateur naïf):

Justification de la spécification d'exception

Les spécifications d'exception [ISO 15.4] sont parfois codées pour indiquer quelles exceptions peuvent être levées, ou parce que le programmeur espère qu'elles amélioreront les performances. Mais considérons le membre suivant à partir d'un pointeur intelligent:

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

Cette fonction n'appelle aucune autre fonction; il ne manipule que les types de données fondamentaux comme les pointeurs. Par conséquent, aucun comportement d'exécution de la spécification d'exception ne peut être invoqué. La fonction est complètement exposée au compilateur; en effet, il est déclaré en ligne. Par conséquent, un compilateur intelligent peut facilement déduire que les fonctions sont incapables de lever des exceptions, et faire les mêmes optimisations qu'il aurait faites sur la base de la spécification d'exception vide. Un compilateur "stupide", cependant, peut faire toutes sortes de pessimisations.

Par exemple, certains compilateurs désactivent l'inline s'il existe une spécification d'exception. Certains compilateurs ajoutent des blocs try / catch. De telles pessimisations peuvent être un désastre de performance qui rend le code inutilisable dans les applications pratiques.

Bien qu'initialement attrayante, une spécification d'exception a tendance à avoir des conséquences qui nécessitent une réflexion très approfondie pour être comprises. Le plus gros problème avec les spécifications d'exception est que les programmeurs les utilisent comme s'ils avaient l'effet souhaité par le programmeur, au lieu de l'effet qu'ils ont réellement.

Une fonction non en ligne est le seul endroit où une spécification d'exception «ne lance rien» peut avoir certains avantages avec certains compilateurs.

Pablo Adames
la source
1
"Le plus gros problème avec les spécifications d'exception est que les programmeurs les utilisent comme s'ils avaient l'effet souhaité par le programmeur, au lieu de l'effet qu'ils ont réellement." C'est la raison n ° 1 des erreurs lors de la communication avec des machines ou des personnes: la différence entre ce que nous disons et ce que nous voulons dire.
Thagomizer