Exceções como afirmações ou como erros?

10

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 nilcom 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 *objno Obj-C, não há risco de uma desreferência de ponteiro NULL.

Per Johansson
fonte
Imho, seria melhor se você mostrasse um pedaço do código do Objective C como você trabalha com erros lá. Parece que as pessoas estão falando sobre algo diferente do que você está perguntando.
Gangnus
De certa forma, acho que estou procurando justificativas para usar exceções. Desde que eu tenho uma idéia de como eles são implementados, eu sei o quão caro eles podem ser. Mas acho que, se eu conseguir um argumento bom o suficiente para usá-los, vou por esse caminho.
Per Johansson
Parece que, para C ++, você precisa de uma justificativa para não usá- los. De acordo com as reações aqui.
Gangnus
2
Talvez, mas até agora não houve ninguém explicando por que eles são melhores (exceto em comparação com os códigos de erro). Não gosto do conceito de usar exceções para coisas que não são excepcionais, mas é mais instintivo do que baseado em fatos.
Per Johansson
"Não gosto ... de usar exceções para coisas que não são excepcionais" - concordou.
Gangnus

Respostas:

1

C ++ parece preferir usar exceções com mais frequência.

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 a assert(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:

A principal desvantagem do uso de uma exceção para relatar um erro de programação é que você realmente não deseja que o desenrolamento da pilha ocorra quando deseja que o depurador seja iniciado na linha exata em que a violação foi detectada, com o estado da linha intacto. Em resumo: existem erros que você sabe que podem ocorrer (consulte os itens 69 a 75). Para todo o resto que não deveria, e é culpa do programador, se houver, existe assert.

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 throwna 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 trybloco 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".

Ele oferece sobrecarga de tempo de execução zero para uma execução sem erros, que supera até a do tratamento de erros de valor de retorno C. Como compensação, a propagação de uma exceção tem uma grande sobrecarga.

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 ( throwingagora 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 throwem 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:

Set<int> set_intersection(const Set<int>& a, const Set<int>& b)
{
     Set<int> intersection;
     for (int key: a)
     {
          try
          {
              b.find(key);
              intersection.insert(other_key);
          }
          catch (const KeyNotFoundException&)
          {
              // Do nothing.
          }
     }
     return intersection;
}

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 catchbloco 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 falsea um iterador inválido nullptrou 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 ao catchsite analógico .

Questões

Eu teria que ir com 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.

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 estritamente nothrow 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:

if ((err = ApiCall(...)) != success)
{
     // Handle error
}

... 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 throwao 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.

Parece-me que, como os ponteiros são raros, eu precisaria usar sinalizadores de erro interno se optar por evitar exceções.

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 nullptrpalavra-chave. É geralmente considerado imprudente usar ponteiros brutos para possuir / gerenciar memória, se você puder usar algo parecido, unique_ptrconsiderando 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 usarshared_ptrem 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 acontecer throws.

Dragon Energy
fonte
14

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 nilquando quer. Não existe tal coisa nilna 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_eachvai 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 .

DeadMG
fonte
A alternativa não é necessariamente um int simples. Eu provavelmente usaria algo parecido com o NSError ao qual estou acostumado com o Obj-C.
Per Johansson
Ele está comparando não com C, mas com o objetivo C. É uma grande diferença. E seus códigos de erro estão explicando as linhas. Tudo o que você diz está correto, mas não responde de alguma forma a essa pergunta. Nenhuma ofensa significava.
Gangnus
Qualquer pessoa que use o Objective-C obviamente dirá que você está absolutamente errado.
gnasher729
5

As exceções foram inventadas por um motivo, para evitar que todo o seu código tenha a seguinte aparência:

bool success = function1(&result1, &err);
if (!success) {
    error_handler(err);
    return;
}

success = function2(&result2, &err);
if (!success) {
    error_handler(err);
    return;
}

Em vez disso, você obtém algo parecido com o seguinte, com um manipulador de exceções instalado mainou convenientemente localizado:

result1 = function1();
result2 = function2();

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.

Karl Bielefeldt
fonte
11
Isso faria mais sentido se o código sem exceções realmente se parecer com isso, mas geralmente não. Esse padrão aparece quando o error_handler nunca retorna, mas raramente o contrário.
Per Johansson
1

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.

Erik Dietrich
fonte
Eu concordo com você, mas é a razão pela qual a maioria parece não fazer isso me fez fazer a pergunta.
Per Johansson
0

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:

  • Adiciona muito código duplicado
  • Faz uma bagunça no seu código - ou seja: fica mais difícil ler
  • Muitas vezes, pode resultar em erros de ponteiro nulo - ou violações de acesso, dependendo da sua "religião" de programação específica

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..catchdeclaraçã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.

S.Robins
fonte
Obrigado, mas não estou pensando em retornar NULL. Não parece possível diante dos construtores. Como mencionei, provavelmente teria que usar um sinalizador de erro no objeto.
Per Johansson