Respostas interessantes: Embora eu concorde com todas elas (até agora), há possíveis conotações para essa questão que até agora foram completamente desconsideradas.
Se o exemplo simples acima for estendido com a alocação de recursos e, em seguida, a verificação de erros com uma possível liberação de recursos resultante, o quadro pode mudar.
Considere a abordagem ingênua que os iniciantes podem adotar:
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;
}
O que foi dito acima representaria uma versão extrema do estilo de retorno prematuro. Observe como o código se torna muito repetitivo e não pode ser mantido ao longo do tempo, quando sua complexidade aumenta. Hoje em dia, as pessoas podem usar o tratamento de exceções para capturá-los.
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 sugeriu, após olhar o exemplo goto abaixo, usar um switch / case break-less dentro do bloco catch acima. Pode-se alternar (typeof (e)) e, em seguida, falhar nas free_resourcex()
chamadas, mas isso não é trivial e precisa de consideração de design . E lembre-se de que um switch / case sem quebras é exatamente como o goto com rótulos encadeados abaixo ...
Como Mark B apontou, em C ++ é considerado um bom estilo seguir o princípio de Aquisição de Recursos é Inicialização , RAII em resumo. A essência do conceito é usar a instanciação de objetos para adquirir recursos. Os recursos são então liberados automaticamente assim que os objetos saem do escopo e seus destruidores são chamados. Para recursos interdependentes, deve-se tomar cuidado especial para garantir a ordem correta de desalocação e projetar os tipos de objetos de forma que os dados necessários estejam disponíveis para todos os destruidores.
Ou em dias de pré-exceção pode fazer:
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;
}
Mas este exemplo simplificado tem várias desvantagens: Ele pode ser usado apenas se os recursos alocados não dependerem uns dos outros (por exemplo, não poderia ser usado para alocar memória, abrir um filehandle e, em seguida, ler dados do manipulador para a memória ) e não fornece códigos de erro individiais e distinguíveis como valores de retorno.
Para manter o código rápido (!), Compacto e facilmente legível e extensível, Linus Torvalds impôs um estilo diferente para o código do kernel que lida com recursos, mesmo usando o infame goto de uma forma que faz sentido :
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;
}
A essência da discussão nas listas de discussão do kernel é que a maioria dos recursos de linguagem que são "preferidos" sobre a instrução goto são gotos implícitos, como if / else enormes, semelhantes a árvore, manipuladores de exceção, instruções de loop / break / continue, etc. . E os goto's no exemplo acima são considerados ok, uma vez que estão saltando apenas uma pequena distância, têm rótulos claros e liberam o código de outras bagunças para acompanhar as condições de erro. Esta questão também foi discutida aqui no stackoverflow .
No entanto, o que está faltando no último exemplo é uma boa maneira de retornar um código de erro. Eu estava pensando em adicionar um result_code++
após cada free_resource_x()
chamada e retornar esse código, mas isso compensa alguns dos ganhos de velocidade do estilo de codificação acima. E é difícil retornar 0 em caso de sucesso. Talvez eu seja apenas sem imaginação ;-)
Então, sim, eu acho que há uma grande diferença na questão de codificar retornos prematuros ou não. Mas também acho que é aparente apenas em códigos mais complicados, que são mais difíceis ou impossíveis de reestruturar e otimizar para o compilador. O que geralmente acontece quando a alocação de recursos entra em ação.