Sou programador profissional em C e programador amador de Obj-C (OS X). Recentemente, fui tentado a expandir para C ++, devido à sua sintaxe muito rica.
Até agora, a codificação não lidei muito com exceções. Objective-C tem, mas a política da Apple é bastante rigorosa:
Importante Você deve reservar o uso de exceções para programação ou erros inesperados de tempo de execução, como acesso à coleção fora dos limites, tentativas de alterar objetos imutáveis, enviar uma mensagem inválida e perder a conexão com o servidor de janelas.
C ++ parece preferir usar exceções com mais frequência. Por exemplo, o exemplo da wikipedia no RAII lança uma exceção se um arquivo não puder ser aberto. Objective-C return nil
com um erro enviado por um parâmetro de saída. Notavelmente, parece que std :: ofstream pode ser definido de qualquer maneira.
Aqui, nos programadores, encontrei várias respostas, proclamando o uso de exceções em vez de códigos de erro ou não usando exceções . Os primeiros parecem mais prevalentes.
Não encontrei ninguém fazendo um estudo objetivo para C ++. Parece-me que, como os ponteiros são raros, eu precisaria usar sinalizadores de erro interno se optar por evitar exceções. Será muito difícil lidar com isso ou talvez funcione ainda melhor do que exceções? Uma comparação dos dois casos seria a melhor resposta.
Edit: Embora não seja completamente relevante, eu provavelmente deveria esclarecer o que nil
é. Tecnicamente, é o mesmo que NULL
, mas o problema é que é bom enviar uma mensagem para nil
. Então você pode fazer algo como
NSError *err = nil;
id obj = [NSFileHandle fileHandleForReadingFromURL:myurl error:&err];
[obj retain];
mesmo que a primeira chamada retornasse nil
. E como você nunca faz *obj
no Obj-C, não há risco de uma desreferência de ponteiro NULL.
fonte
Respostas:
Eu sugeriria, na verdade, menos do que o Objective-C em alguns aspectos, porque a biblioteca padrão C ++ geralmente não gerava erros de programador, como acesso fora dos limites de uma sequência de acesso aleatório em sua forma de design de caso mais comum (em
operator[]
, por exemplo) ou tentando desreferenciar um iterador inválido. A linguagem não se preocupa em acessar uma matriz fora dos limites ou desreferenciar um ponteiro nulo ou qualquer coisa desse tipo.Tirar os erros do programador em grande parte da equação de tratamento de exceções, na verdade, elimina uma categoria muito grande de erros aos quais outros idiomas costumam responder
throwing
. O C ++ tende aassert
(que não é compilado nas compilações de lançamento / produção, apenas compilações de depuração) ou apenas falha (geralmente travando) nesses casos, provavelmente em parte porque a linguagem não deseja impor o custo dessas verificações de tempo de execução como seria necessário para detectar esses erros do programador, a menos que o programador queira especificamente pagar os custos, escrevendo o código que executa essas verificações ele mesmo.Sutter ainda incentiva a evitar exceções em tais casos nos C ++ Coding Standards:
Essa regra não é necessariamente imutável. Em alguns casos mais críticos, pode ser preferível usar, por exemplo, wrappers e um padrão de codificação que registre uniformemente onde ocorrem erros do programador e
throw
na presença de erros do programador, como tentar deferir algo inválido ou acessá-lo fora dos limites, porque pode ser muito caro não conseguir recuperar nesses casos, se o software tiver uma chance. Mas, em geral, o uso mais comum da linguagem tende a favorecer o não lançamento de erros de programadores.Exceções externas
Onde eu vejo exceções incentivadas com mais freqüência em C ++ (de acordo com o comitê padrão, por exemplo) é para "exceções externas", como um resultado inesperado em alguma fonte externa fora do programa. Um exemplo está falhando ao alocar memória. Outro está falhando ao abrir um arquivo crítico necessário para a execução do software. Outro está falhando ao se conectar a um servidor necessário. Outro é um usuário pressionando um botão de cancelamento para cancelar uma operação cujo caminho de execução de caso comum espera obter êxito, sem essa interrupção externa. Todas essas coisas estão fora do controle do software imediato e dos programadores que o criaram. São resultados inesperados de fontes externas que impedem que a operação (que realmente deve ser considerada uma transação indivisível no meu livro *) seja bem-sucedida.
Transações
Eu geralmente incentivo a olhar para um
try
bloco como uma "transação" porque as transações devem ter sucesso como um todo ou falhar como um todo. Se estivermos tentando fazer alguma coisa e ela falhar no meio do caminho, todos os efeitos colaterais / mutações feitos no estado do programa geralmente precisam ser revertidos para colocar o sistema novamente em um estado válido, como se a transação nunca tivesse sido executada, assim como um RDBMS que falha ao processar uma consulta no meio não deve comprometer a integridade do banco de dados. Se você alterar o estado do programa diretamente na transação, deverá "desativá-lo" ao encontrar um erro (e aqui os protetores de escopo podem ser úteis com o RAII).A alternativa muito mais simples é não alterar o estado original do programa; você pode alterar uma cópia dela e, se for bem-sucedida, troque a cópia pelo original (garantindo que a troca não seja possível). Se falhar, descarte a cópia. Isso também se aplica mesmo se você não usar exceções para o tratamento de erros em geral. Uma mentalidade "transacional" é essencial para a recuperação adequada se ocorrerem mutações no estado do programa antes de encontrar um erro. Ou é bem-sucedido como um todo ou falha como um todo. Ele ainda não conseguiu fazer suas mutações.
Este é estranhamente um dos tópicos menos discutidos com frequência quando vejo programadores perguntando sobre como manipular corretamente erros ou exceções, mas é o mais difícil de todos eles acertar em qualquer software que queira alterar diretamente o estado do programa em muitos suas operações. Pureza e imutabilidade podem ajudar aqui a obter segurança de exceção tanto quanto ajudam na segurança de threads, pois um efeito colateral externo / mutação que não ocorre não precisa ser revertido.
atuação
Outro fator norteador no uso ou não de exceções é o desempenho, e não me refiro de uma maneira obsessiva, penny beliscante e contraproducente. Muitos compiladores C ++ implementam o que é chamado de "Tratamento de exceção de custo zero".
De acordo com o que eu li sobre isso, faz com que os caminhos de execução de caso comuns não exijam sobrecarga (nem mesmo a sobrecarga que normalmente acompanha a manipulação e propagação do código de erro no estilo C), em troca de distorcer os custos em direção aos caminhos excepcionais (
throwing
agora significa mais caro do que nunca)."Caro" é um pouco difícil de quantificar, mas, para começar, você provavelmente não quer jogar um milhão de vezes em um loop apertado. Esse tipo de design pressupõe que as exceções não ocorram esquerda e direita o tempo todo.
Não erros
E esse ponto de desempenho me leva a não erros, o que é surpreendentemente impreciso se olharmos para todos os tipos de outros idiomas. Mas eu diria que, dado o design de EH de custo zero mencionado acima, você quase certamente não deseja
throw
em resposta a uma chave que não foi encontrada em um conjunto. Porque isso não é apenas indiscutivelmente um erro (a pessoa que está pesquisando a chave pode ter construído o conjunto e espera estar pesquisando por chaves que nem sempre existem), mas seria muito caro nesse contexto.Por exemplo, uma função de interseção de conjunto pode querer percorrer dois conjuntos e procurar as chaves que eles têm em comum. Se não conseguir encontrar uma chave
threw
, você percorrerá e poderá encontrar exceções em metade ou mais das iterações:O exemplo acima é absolutamente ridículo e exagerado, mas vi no código de produção algumas pessoas provenientes de outras linguagens que usam exceções em C ++, mais ou menos assim, e acho que é uma afirmação razoavelmente prática de que esse não é um uso apropriado de exceções em C ++. Outra dica acima é que você notará que o
catch
bloco não tem absolutamente nada a ver e foi escrito apenas para ignorar forçosamente essas exceções, e isso geralmente é uma dica (embora não seja uma garantia) de que as exceções provavelmente não estão sendo usadas de maneira apropriada no C ++.Para esses tipos de casos, algum tipo de valor de retorno indicando falha (qualquer coisa, desde retornar
false
a um iterador inválidonullptr
ou qualquer outra coisa que faça sentido no contexto) é geralmente muito mais apropriado e também frequentemente mais prático e produtivo, pois um tipo de erro sem erro O caso geralmente não exige que algum processo de desenrolamento de pilha chegue aocatch
site analógico .Questões
Evitar exceções imediatas em C ++ parece extremamente contraproducente para mim, a menos que você esteja trabalhando em algum sistema incorporado ou em um tipo específico de caso que proíba o uso deles (nesse caso, você também teria que se esforçar para evitar tudo biblioteca e idioma que, de outra forma
throw
, gostariam de usar estritamentenothrow
new
).Se você absolutamente precisa evitar exceções por qualquer motivo (por exemplo, trabalhando nos limites da API C de um módulo cuja API C você exporta), muitos podem discordar de mim, mas eu sugiro usar um manipulador / status global de erros como o OpenGL
glGetError()
. Você pode fazê-lo usar o armazenamento local do encadeamento para ter um status de erro exclusivo por encadeamento.Minha justificativa é que não estou acostumado a ver equipes em ambientes de produção verificar minuciosamente todos os erros possíveis, infelizmente, quando códigos de erro são retornados. Se eles foram completos, algumas APIs C podem encontrar um erro em praticamente todas as chamadas da API C, e a verificação completa exigiria algo como:
... com quase todas as linhas de código que invocam a API que exige essas verificações. Ainda não tive a sorte de trabalhar com equipes tão completas. Eles geralmente ignoram esses erros na metade, às vezes até na maior parte do tempo. Esse é o maior apelo para mim das exceções. Se envolvermos essa API e a fizermos uniformemente
throw
ao encontrar um erro, a exceção não poderá ser ignorada e, na minha opinião, e experiência, é aí que reside a superioridade das exceções.Mas se as exceções não puderem ser usadas, o status de erro global por thread, pelo menos, tem a vantagem (enorme em comparação com o retorno de códigos de erro para mim) de que ele pode ter a chance de detectar um erro anterior um pouco mais tarde do que quando ocorreu em alguma base de código desleixada, em vez de perdê-la completamente e nos deixar completamente alheios ao que aconteceu. O erro pode ter ocorrido algumas linhas antes ou em uma chamada de função anterior, mas, desde que o software ainda não tenha travado, poderemos começar a trabalhar de trás para frente e descobrir onde e por que ocorreu.
Eu não diria necessariamente que indicadores são raros. Atualmente, existem métodos no C ++ 11 e posteriores para obter os ponteiros de dados subjacentes dos contêineres e uma nova
nullptr
palavra-chave. É geralmente considerado imprudente usar ponteiros brutos para possuir / gerenciar memória, se você puder usar algo parecido,unique_ptr
considerando o quão crítico é estar em conformidade com RAII na presença de exceções. Mas indicadores brutos que não possuem / gerenciam memória não são necessariamente considerados tão ruins (mesmo de pessoas como Sutter e Stroustrup) e, às vezes, muito práticos como uma maneira de apontar para as coisas (junto com índices que apontam para as coisas).Eles são indiscutivelmente menos seguros que os iteradores de contêiner padrão (pelo menos na versão, iteradores verificados ausentes) que não serão detectados se você tentar desreferenciá-los depois que eles forem invalidados. O C ++ ainda é, sem vergonha, uma linguagem perigosa, eu diria, a menos que seu uso específico queira agrupar tudo e ocultar até mesmo ponteiros brutos que não sejam proprietários. É quase crítico, com exceções, que os recursos estejam em conformidade com o RAII (que geralmente não tem custo de tempo de execução), mas, além disso, não está necessariamente tentando ser a linguagem mais segura para evitar custos que um desenvolvedor não deseja explicitamente. trocar por outra coisa. O uso recomendado não está tentando protegê-lo de coisas como ponteiros pendurados e iteradores invalidados, por assim dizer (caso contrário, seremos incentivados a usar
shared_ptr
em todo o lugar, que Stroustrup se opõe veementemente). Ele está tentando protegê-lo de deixar de liberar / liberar / destruir / desbloquear / limpar adequadamente um recurso quando algo acontecerthrows
.fonte
A questão é a seguinte: devido ao histórico e à flexibilidade únicos do C ++, você pode encontrar alguém proclamando praticamente qualquer opinião sobre qualquer recurso que desejar. No entanto, em geral, quanto mais o que você estiver fazendo parecer C, pior será a ideia.
Uma das razões pelas quais o C ++ é muito mais flexível quando se trata de exceções é que você não pode exatamente
return nil
quando quer. Não existe tal coisanil
na grande maioria dos casos e tipos.Mas aqui está o fato simples. Exceções fazem seu trabalho automaticamente. Quando você lança uma exceção, o RAII assume o controle e tudo é tratado pelo compilador. Quando você usa códigos de erro, você tem que lembrar de verificá-los. Isso inerentemente torna as exceções significativamente mais seguras que os códigos de erro - elas se verificam, por assim dizer. Além disso, eles são mais expressivos. Lance uma exceção e você pode obter uma string informando qual é o erro e pode até conter informações específicas, como "Parâmetro incorreto X que tinha valor Y em vez de Z". Obtenha um código de erro 0xDEADBEEF e o que exatamente deu errado? Espero, com certeza, que a documentação esteja completa e atualizada, e mesmo se você receber "Parâmetro inválido", isso nunca lhe dirá qualparâmetro, que valor tinha e que valor deveria ter. Se você pegar por referência, também, eles podem ser polimórficos. Finalmente, podem ser geradas exceções em locais onde os códigos de erro nunca podem, como construtores. E quanto aos algoritmos genéricos? Como
std::for_each
vai lidar com o seu código de erro? Dica profissional: não é.As exceções são muito superiores aos códigos de erro em todos os aspectos. A verdadeira questão está em exceções versus afirmações.
Aqui está a coisa. Ninguém pode saber com antecedência quais pré-condições seu programa possui para operar, quais condições de falha são incomuns, quais podem ser verificadas antecipadamente e quais são bugs. Isso geralmente significa que você não pode decidir antecipadamente se uma determinada falha deve ser uma afirmação ou uma exceção sem conhecer a lógica do programa. Além disso, uma operação que pode continuar quando uma de suas suboperações falha é a exceção , não a regra.
Na minha opinião, há exceções a serem capturadas. Não necessariamente imediatamente, mas eventualmente. Uma exceção é um problema que você espera que o programa possa recuperar em algum momento. Mas a operação em questão nunca pode se recuperar de um problema que justifique uma exceção.
Falhas de asserção são sempre fatais, erros irrecuperáveis. Corrupção de memória, esse tipo de coisa.
Então, quando você não pode abrir um arquivo, é uma afirmação ou exceção? Bem, em uma biblioteca genérica, existem muitos cenários em que o erro pode ser tratado, por exemplo, ao carregar um arquivo de configuração, você pode simplesmente usar um padrão pré-criado, então eu diria exceção.
Como nota de rodapé, eu gostaria de mencionar que há algo sobre "Padrão de Objeto Nulo" por aí. Esse padrão é terrível . Em dez anos, será o próximo Singleton. O número de casos em que você pode produzir um objeto nulo adequado é minúsculo .
fonte
As exceções foram inventadas por um motivo, para evitar que todo o seu código tenha a seguinte aparência:
Em vez disso, você obtém algo parecido com o seguinte, com um manipulador de exceções instalado
main
ou convenientemente localizado:Algumas pessoas reivindicam benefícios de desempenho para a abordagem sem exceção, mas, na minha opinião, as preocupações com a legibilidade superam os pequenos ganhos de desempenho, especialmente quando você inclui o tempo de execução de todo o
if (!success)
padrão necessário para espalhar por todo o lado, ou o risco de se depurar segfaults se você não ' inclua e considerando as chances de ocorrência de exceções são relativamente raras.Não sei por que a Apple desencoraja o uso de exceções. Se eles estão tentando evitar a propagação de exceções não tratadas, tudo o que você realmente realiza são pessoas que usam ponteiros nulos para indicar exceções. Portanto, um erro do programador resulta em uma exceção de ponteiro nulo em vez de em um arquivo muito mais útil que não é uma exceção encontrada ou o que seja.
fonte
No post que você faz referência (exceções versus códigos de erro), acho que há uma discussão sutilmente diferente em andamento. A questão parece ser se você tem alguma lista global de #define códigos de erro, provavelmente completa com nomes como ERR001_FILE_NOT_WRITEABLE (ou abreviado, se você não tiver sorte). E acho que o ponto principal desse segmento é que, se você estiver programando em uma linguagem polimórfica, usando instâncias de objeto, essa construção processual não será necessária. As exceções podem expressar qual erro está ocorrendo simplesmente em virtude de seu tipo e podem encapsular informações como a mensagem a ser impressa (e outras coisas também). Portanto, eu consideraria essa conversa como se você deveria programar proceduralmente em uma linguagem orientada a objetos ou não.
Porém, a conversa sobre quando e quanto depender de exceções para lidar com situações que surgem no código é diferente. Quando exceções são lançadas e capturadas, você está introduzindo um paradigma de fluxo de controle completamente diferente da pilha de chamadas tradicional. O lançamento de exceção é basicamente uma instrução goto que o remove da pilha de chamadas e o envia para um local indeterminado (onde quer que seu código de cliente decida lidar com sua exceção). Isso faz com que a lógica de exceção muito difícil raciocinar sobre .
E assim, há um sentimento como o expresso por jheriko de que estes devem ser minimizados. Ou seja, digamos que você esteja lendo um arquivo que pode ou não existir no disco. Jheriko e Apple e aqueles que pensam como pensam (inclusive eu) argumentam que lançar uma exceção não é apropriado - a ausência desse arquivo não é excepcional , é esperado. Exceções não devem substituir o fluxo de controle normal. É igualmente fácil fazer com que o método ReadFile () retorne, digamos, um valor booleano e faça com que o código do cliente veja no valor de retorno false que o arquivo não foi encontrado. O código do cliente pode dizer que o arquivo do usuário não foi encontrado ou lidar com esse caso silenciosamente ou o que ele quiser fazer.
Quando você lança uma exceção, está forçando um fardo para seus clientes. Você está dando a eles um código que, às vezes, os remove da pilha de chamadas normal e os força a escrever um código adicional para impedir que seu aplicativo falhe. Um fardo tão poderoso e dissonante só deve ser imposto a eles se for absolutamente necessário em tempo de execução ou no interesse da filosofia "falhar cedo". Portanto, no primeiro caso, você pode lançar exceções se o objetivo do seu aplicativo é monitorar alguma operação de rede e alguém desconectar o cabo de rede (você está sinalizando uma falha catastrófica). No último caso, você poderá lançar uma exceção se seu método esperar um parâmetro não nulo e for entregue nulo (você está sinalizando que está sendo usado de forma inadequada).
Quanto ao que fazer, em vez de exceções no mundo orientado a objetos, você tem opções além da construção do código de erro processual. Um que vem à mente imediatamente é que você pode criar objetos chamados OperationResult ou algo parecido. Um método pode retornar um OperationResult e esse objeto pode ter informações sobre se a operação foi bem-sucedida ou não, e qualquer outra informação que você deseja que ela tenha (uma mensagem de status, por exemplo, mas também, mais sutilmente, pode encapsular estratégias para recuperação de erros ) Retornar isso em vez de lançar uma exceção permite polimorfismo, preserva o fluxo de controle e torna a depuração muito mais simples.
fonte
Há um adorável par de capítulos no Clean Code que falam sobre esse assunto - menos o ponto de vista da Apple.
A idéia básica é que você nunca retorne nada de suas funções, porque:
A outra ideia que acompanha é que você usa exceções, pois ajuda a reduzir ainda mais a confusão de seu código e, em vez de precisar criar uma série de códigos de erro que eventualmente se tornam difíceis de rastrear, gerenciar e manter (principalmente em vários módulos e plataformas) e Para os seus clientes decifrar, você cria mensagens significativas que identificam a natureza exata do problema, simplificam a depuração e podem reduzir o código de tratamento de erros a algo tão simples quanto uma
try..catch
declaração básica .Nos meus dias de desenvolvimento de COM, eu tinha um projeto em que cada falha gerava uma exceção, e cada exceção precisava usar um código de erro exclusivo, baseado na extensão de exceções COM padrão do Windows. Era um impedimento de um projeto anterior, onde os códigos de erro haviam sido usados anteriormente, mas a empresa queria seguir toda a orientação a objetos e pular no movimento da banda COM sem alterar muito a maneira como eles faziam as coisas. Levou apenas quatro meses para eles ficarem sem números de código de erro e coube a mim refatorar grandes extensões de código para usar exceções. Melhor poupar o esforço antecipadamente e usar exceções criteriosamente.
fonte