Réponses intéressantes: Bien que je sois d'accord avec tous (jusqu'à présent), il y a des connotations possibles à cette question qui sont jusqu'à présent complètement ignorées.
Si l'exemple simple ci-dessus est étendu avec l'allocation de ressources, puis la vérification des erreurs avec une libération potentielle de ressources, l'image peut changer.
Considérez l' approche naïve que les débutants pourraient adopter:
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;
}
Ce qui précède représenterait 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, les gens peuvent utiliser la gestion 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 catch ci-dessus. On pourrait basculer (typeof (e)) puis passer à travers les free_resourcex()
appels, mais ce n'est pas trivial et nécessite une considération de conception . Et rappelez-vous qu'un interrupteur / boîtier sans pause est exactement comme le goto avec les étiquettes en guirlande ci-dessous ...
Comme l'a souligné Mark B, en C ++, il est considéré comme un bon style de suivre le principe d' acquisition de ressources est l'initialisation , RAII en bref. L'essentiel du concept est d'utiliser l'instanciation d'objets pour acquérir des ressources. Les ressources sont alors automatiquement libérées dès que les objets sont hors de portée et que leurs destructeurs sont appelés. Pour les ressources interdépendantes, un soin particulier doit être pris pour garantir le bon ordre de désallocation et pour concevoir les types d'objets de telle sorte que les données requises soient disponibles pour tous les destructeurs.
Ou dans les jours 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 simplifié 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 peut pas être utilisé pour allouer de la mémoire, puis ouvrir un descripteur de fichier, puis lire les données du descripteur dans la mémoire ), et il ne fournit pas de codes d'erreur individuels et distinctifs comme valeurs de retour.
Pour garder le code rapide (!), Compact et facilement lisible et extensible, Linus Torvalds a imposé un style différent pour le code du noyau qui traite des ressources, même en utilisant le tristement célèbre goto d'une manière absolument 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 fonctionnalités du langage qui sont "préférées" par rapport à l'instruction goto sont des gotos implicites, comme les énormes if / else en forme d'arbre, les gestionnaires d'exceptions, les instructions loop / break / continue, etc. Et les goto dans l'exemple ci-dessus sont considérés comme ok, car ils ne sautent que sur une petite distance, ont des étiquettes claires et libèrent le code de tout autre encombrement pour garder une trace des conditions d'erreur. Cette question a également été abordée ici sur stackoverflow .
Cependant, ce qui manque dans le dernier exemple est une bonne façon de renvoyer un code d'erreur. Je pensais ajouter unresult_code++
après chaque free_resource_x()
appel et renvoyer ce code, mais cela compense certains des gains de vitesse du style de codage ci-dessus. Et il est difficile de renvoyer 0 en cas de succès. Peut-être que je suis juste sans imagination ;-)
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 que cela n'apparaît 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 une fois que l'allocation des ressources entre en jeu.