Réponses intéressantes : Bien que je sois d'accord avec toutes les réponses (jusqu'à présent), il y a des connotations possibles à cette question qui sont jusqu'à présent complètement ignorées.
Si l'on ajoute à l'exemple simple ci-dessus l'allocation de ressources, puis le contrôle d'erreurs et la libération potentielle de ressources qui en résulte, le tableau peut changer.
Considérez le approche naïve que les débutants pourraient prendre :
int func(..some parameters...) {
res_a a = allocate_resource_a();
if (!a) {
return 1;
}
res_b b = allocate_resource_b();
if (!b) {
free_resource_a(a);
return 2;
}
res_c c = allocate_resource_c();
if (!c) {
free_resource_b(b);
free_resource_a(a);
return 3;
}
do_work();
free_resource_c(c);
free_resource_b(b);
free_resource_a(a);
return 0;
}
L'exemple ci-dessus représente une version extrême du style de retour prématuré. Remarquez comment le code devient très répétitif et non maintenable au fil du temps lorsque sa complexité augmente. De nos jours, on peut utiliser traitement des exceptions pour les attraper.
int func(..some parameters...) {
res_a a;
res_b b;
res_c c;
try {
a = allocate_resource_a(); # throws ExceptionResA
b = allocate_resource_b(); # throws ExceptionResB
c = allocate_resource_c(); # throws ExceptionResC
do_work();
}
catch (ExceptionBase e) {
# Could use type of e here to distinguish and
# use different catch phrases here
# class ExceptionBase must be base class of ExceptionResA/B/C
if (c) free_resource_c(c);
if (b) free_resource_b(b);
if (a) free_resource_a(a);
throw e
}
return 0;
}
Philip a suggéré, après avoir regardé l'exemple goto ci-dessous, d'utiliser un interrupteur/boîtier sans rupture à l'intérieur du bloc de capture ci-dessus. On pourrait commuter(typeof(e)) et ensuite tomber dans le bloc free_resourcex()
appels mais c'est n'est pas anodin et nécessite une réflexion sur la conception . Et rappelez-vous qu'un interrupteur/case sans ruptures est exactement comme le goto avec des étiquettes en guirlande ci-dessous...
Comme Mark B l'a fait remarquer, en C++, il est considéré comme un bon style de suivre la règle du L'acquisition des ressources est l'initialisation principe, RAII en bref. L'essentiel du concept consiste à utiliser l'instanciation d'un objet pour acquérir des ressources. Les ressources sont ensuite automatiquement libérées dès que les objets sortent de leur champ d'application et que leurs destructeurs sont appelés. Pour les ressources interdépendantes, il convient de veiller à l'ordre correct de la libération et de concevoir les types d'objets de telle sorte que les données requises soient disponibles pour tous les destructeurs.
Ou dans les jours de pré-exception pourrait faire :
int func(..some parameters...) {
res_a a = allocate_resource_a();
res_b b = allocate_resource_b();
res_c c = allocate_resource_c();
if (a && b && c) {
do_work();
}
if (c) free_resource_c(c);
if (b) free_resource_b(b);
if (a) free_resource_a(a);
return 0;
}
Mais cet exemple trop simpliste présente plusieurs inconvénients : Il ne peut être utilisé que si les ressources allouées ne dépendent pas les unes des autres (par exemple, il ne pourrait pas être utilisé pour allouer de la mémoire, puis ouvrir un gestionnaire de fichier, puis lire les données du gestionnaire dans la mémoire), et il ne fournit pas de codes d'erreur individuels et distincts comme valeurs de retour.
Maintenir un code rapide ( !), compact, facilement lisible et extensible. Linus Torvalds a imposé un style différent pour le code du noyau qui traite des ressources, utilisant même le tristement célèbre goto d'une manière qui est tout à fait logique :
int func(..some parameters...) {
res_a a;
res_b b;
res_c c;
a = allocate_resource_a() || goto error_a;
b = allocate_resource_b() || goto error_b;
c = allocate_resource_c() || goto error_c;
do_work();
error_c:
free_resource_c(c);
error_b:
free_resource_b(b);
error_a:
free_resource_a(a);
return 0;
}
L'essentiel de la discussion sur les listes de diffusion du noyau est que la plupart des caractéristiques du langage qui sont "préférées" à l'instruction goto sont des gotos implicites, comme les énormes if/else arborescents, les gestionnaires d'exceptions, les instructions de type boucle/break/continue, etc. Et les goto dans l'exemple ci-dessus sont considérés comme corrects, puisqu'ils ne sautent que sur une petite distance, ont des étiquettes claires, et libèrent le code d'autres encombrements pour garder la trace des conditions d'erreur. Cette question a également été discutée ici sur stackoverflow .
Cependant, ce qui manque dans le dernier exemple, c'est une façon agréable de renvoyer un code d'erreur. Je pensais ajouter un result_code++
après chaque free_resource_x()
et de retourner ce code, mais cela annule certains des gains de vitesse du style de codage ci-dessus. Et il est difficile de retourner 0 en cas de succès. Peut-être que je suis juste peu imaginatif ;-)
Donc, oui, je pense qu'il y a une grande différence dans la question du codage des retours prématurés ou non. Mais je pense aussi qu'elle n'est apparente que dans un code plus compliqué qu'il est plus difficile ou impossible de restructurer et d'optimiser pour le compilateur. Ce qui est généralement le cas lorsque l'allocation des ressources entre en jeu.