O FAQ de exceção isocpp.org estados
Não use throw para indicar um erro de codificação no uso de uma função. Use assert ou outro mecanismo para enviar o processo para um depurador ou para travar o processo e coletar o despejo de falha para o desenvolvedor depurar.
Por outro lado, a biblioteca padrão define std :: logic_error e todas as suas derivadas, que me parecem que devem tratar, além de outras coisas, erros de programação. A passagem de uma string vazia para std :: stof (lançará um argumento inválido) não é um erro de programação? A passagem de uma string que contém caracteres diferentes de '1' / '0' para std :: bitset (lançará um argumento inválido) não é um erro de programação? A chamada std :: bitset :: set com um índice inválido (lançará fora do intervalo) não é um erro de programação? Se não forem, qual é o erro de programação que alguém testaria? O construtor baseado em string std :: bitset existe apenas desde C ++ 11, portanto, deveria ter sido projetado com o uso idiomático de exceções em mente. Por outro lado, as pessoas me dizem que logic_error basicamente não deve ser usado.
Outra regra que surge frequentemente com exceções é "use somente exceções em circunstâncias excepcionais". Mas como uma função de biblioteca deve saber quais circunstâncias são excepcionais? Para alguns programas, não é possível abrir um arquivo pode ser excepcional. Para outros, não é possível alocar memória pode não ser excepcional. E há centenas de casos no meio. Ser incapaz de criar um soquete? Não foi possível conectar ou gravar dados em um soquete ou arquivo? Não foi possível analisar a entrada? Pode ser excepcional, pode não ser. Definitivamente, a função em si definitivamente não pode saber, não tem idéia de que tipo de contexto está sendo chamada.
Então, como devo decidir se devo usar exceções ou não para uma função específica? Parece-me que a única maneira realmente consistente é usá-los para todo e qualquer tratamento de erros, ou para nada. E se eu estiver usando a biblioteca padrão, essa escolha foi feita para mim.
fonte
Respostas:
Primeiro, sinto-me obrigado a salientar que
std::exception
e seus filhos foram projetados há muito tempo. Existem várias partes que provavelmente (quase certamente) seriam diferentes se estivessem sendo projetadas hoje.Não me entenda mal: há partes do design que funcionaram muito bem e são bons exemplos de como criar uma hierarquia de exceções para C ++ (por exemplo, o fato de que, diferentemente da maioria das outras classes, todas compartilham um raiz comum).
Olhando especificamente
logic_error
, temos um pouco de um enigma. Por um lado, se você tiver alguma escolha razoável no assunto, o conselho que você citou está correto: geralmente é melhor falhar o mais rápido e ruidosamente possível, para que possa ser depurado e corrigido.Para melhor ou para pior, no entanto, é difícil definir a biblioteca padrão em torno do que você geralmente deve fazer. Se eles os definissem para sair do programa (por exemplo, chamar
abort()
) quando recebessem informações incorretas, isso seria o que sempre acontecia para essa circunstância - e na verdade existem algumas circunstâncias em que essa provavelmente não é a coisa certa a ser feita. , pelo menos no código implantado.Isso se aplicaria ao código com requisitos de tempo real (pelo menos moderados) e penalidade mínima por uma saída incorreta. Por exemplo, considere um programa de bate-papo. Se estiver decodificando alguns dados de voz e obtendo uma entrada incorreta, é provável que o usuário fique muito mais feliz em viver com um milissegundo de estática na saída do que um programa que é completamente desligado. Da mesma forma, ao executar a reprodução de vídeo, pode ser mais aceitável produzir valores incorretos para alguns pixels de um quadro ou dois do que o programa sair sumariamente porque o fluxo de entrada foi corrompido.
Quanto ao uso de exceções para relatar certos tipos de erros: você está certo - a mesma operação pode ser qualificada como uma exceção ou não, dependendo de como está sendo usada.
Por outro lado, você também está errado - o uso da biblioteca padrão não força (necessariamente) essa decisão sobre você. No caso de abrir um arquivo, você normalmente usaria um iostream. Os Iostreams também não são exatamente o melhor e o mais recente, mas, neste caso, eles acertam: permitem definir um modo de erro, para que você possa controlar se não abrir um arquivo com o resultado de uma exceção ser lançada ou não. Portanto, se você tiver um arquivo que é realmente necessário para o seu aplicativo e, ao não abri-lo, precisará executar algumas ações corretivas sérias, e poderá criar uma exceção se ele não conseguir abrir o arquivo. Para a maioria dos arquivos, que você tentará abrir, se eles não existirem ou não estiverem acessíveis, eles simplesmente falharão (esse é o padrão).
Quanto à sua decisão: não acho que haja uma resposta fácil. Para o bem ou para o mal, nem sempre é fácil medir "circunstâncias excepcionais". Embora certamente haja casos fáceis de decidir devem ser [não] excepcionais, há (e provavelmente sempre haverá) casos em que isso pode ser questionado ou requer conhecimento de contexto fora do domínio da função em questão. Para casos como esse, pode pelo menos valer a pena considerar um design semelhante a esta parte do iostreams, em que o usuário pode decidir se a falha resulta em uma exceção sendo lançada ou não. Como alternativa, é inteiramente possível ter dois conjuntos separados de funções (ou classes etc.), uma das quais lançará exceções para indicar falha, a outra utilizará outros meios. Se você seguir esse caminho,
fonte
Você pode não acreditar nisso, mas, bem, diferentes codificadores C ++ discordam. É por isso que o FAQ diz uma coisa, mas a biblioteca padrão discorda.
O FAQ defende a falha porque será mais fácil depurar. Se você travar e obter um dump principal, terá o estado exato do seu aplicativo. Se você lançar uma exceção, perderá muito desse estado.
A biblioteca padrão adota a teoria de que dar ao codificador a capacidade de capturar e manipular o erro é mais importante que a depuração.
A idéia aqui é que, se sua função não souber se a situação é excepcional ou não, ela não deve gerar uma exceção. Ele deve retornar um estado de erro por meio de outro mecanismo. Quando atingir um ponto no programa em que sabe que o estado é excepcional, deve lançar a exceção.
Mas isso tem seu próprio problema. Se um estado de erro for retornado de uma função, você pode não se lembrar de verificá-lo e o erro passará silenciosamente. Isso leva algumas pessoas a abandonar as exceções, sendo uma regra excepcional a favor de lançar exceções para qualquer tipo de estado de erro.
No geral, o ponto principal é que pessoas diferentes têm idéias diferentes sobre quando lançar exceções. Você não encontrará uma única ideia coesa. Mesmo que algumas pessoas afirmem dogmaticamente que essa ou aquela é a maneira correta de lidar com exceções, não existe uma teoria acordada.
Você pode lançar exceções:
e encontre alguém na internet que concorde com você. Você terá que adotar o estilo que funciona para você.
fonte
Muitas outras boas respostas foram escritas, só quero acrescentar um breve ponto.
A resposta tradicional, especialmente quando a FAQ do ISO C ++ foi escrita, compara principalmente "exceção C ++" vs. "código de retorno no estilo C". Uma terceira opção ", retorna algum tipo de valor composto, por exemplo, a
struct
ouunion
, atualmente,boost::variant
ou o (proposto)std::expected
, não é considerado.Antes do C ++ 11, a opção "retornar um tipo composto" era geralmente muito fraca. Como não havia semântica de movimento, copiar as coisas dentro e fora de uma estrutura era potencialmente muito caro. Era extremamente importante naquele ponto do idioma modelar seu código no RVO para obter o melhor desempenho. As exceções eram como uma maneira fácil de retornar efetivamente um tipo composto, caso contrário isso seria bastante difícil.
A IMO, após o C ++ 11, essa opção "retornar uma união discriminada", semelhante ao idioma
Result<T, E>
usado atualmente no Rust, deve ser favorecida com mais frequência no código C ++. Às vezes, é realmente um estilo mais simples e conveniente de indicar erros. Com exceções, sempre existe uma possibilidade de que funções que não foram lançadas antes poderiam começar a ser repentinamente executadas após um refator e os programadores nem sempre documentam essas coisas tão bem. Quando o erro é indicado como parte do valor de retorno em uma união discriminada, reduz bastante a chance de o programador simplesmente ignorar o código do erro, que é a crítica usual ao tratamento de erros no estilo C.Geralmente
Result<T, E>
funciona como um impulso opcional. Você pode testar, usandooperator bool
, se é um valor ou um erro. E então use sayoperator *
para acessar o valor, ou alguma outra função "get". Geralmente esse acesso é desmarcado, para velocidade. Mas você pode fazer com que, em uma compilação de depuração, o acesso seja verificado e uma asserção garanta que haja realmente um valor e não um erro. Dessa forma, qualquer pessoa que não verifique corretamente os erros terá uma afirmação rígida, em vez de um problema mais insidioso.Uma vantagem adicional é que, diferentemente das exceções em que, se não for capturado, apenas voa a pilha a alguma distância arbitrária, com esse estilo, quando uma função começa a sinalizar um erro que não ocorria antes, você não pode compilar, a menos que o código é alterado para lidar com isso. Isso torna os problemas mais altos - o problema tradicional de "exceção não capturada" se torna mais um erro em tempo de compilação do que um erro em tempo de execução.
Eu me tornei um grande fã desse estilo. Normalmente, hoje em dia uso isso ou exceções. Mas tento limitar as exceções a grandes problemas. Para algo como um erro de análise, tento retornar,
expected<T>
por exemplo. Coisas comostd::stoi
eboost::lexical_cast
que lançam uma exceção C ++ no caso de algum problema relativamente menor "a string não poder ser convertida em número" me parecem muito ruins hoje em dia.fonte
std::expected
ainda é uma proposta não aceita, certo?exception_ptr
, ou você deseja apenas usar algum tipo de estrutura ou algo assim Curtiu isso.[[nodiscard]] attribute
será útil para essa abordagem de tratamento de erros, pois garante que você simplesmente não ignore o resultado do erro por acidente.except_ptr
), você tinha que lançar uma exceção internamente. Pessoalmente, acho que essa ferramenta deve funcionar completamente independente das execuções. Apenas uma observação.Essa é uma questão altamente subjetiva, pois faz parte do design. E como o design é basicamente arte, prefiro discutir essas coisas em vez de debater (não estou dizendo que você está debatendo).
Para mim, casos excepcionais são de dois tipos - aqueles que lidam com recursos e aqueles que lidam com operações críticas. O que pode ser considerado crítico depende do problema em questão e, em muitos casos, do ponto de vista do programador.
A falha na aquisição de recursos é um dos principais candidatos a lançar exceções. O recurso pode ser memória, arquivo, conexão de rede ou qualquer outra coisa com base no seu problema e plataforma. Agora, a falha ao liberar um recurso justifica uma exceção? Bem, isso depende novamente. Não fiz nada em que a liberação da memória falhou, por isso não tenho certeza sobre esse cenário. No entanto, a exclusão de arquivos como parte da liberação de recursos pode falhar e falhou para mim, e essa falha geralmente está vinculada a outro processo, mantendo-o aberto em um aplicativo de processo múltiplo. Eu acho que outros recursos podem falhar durante o lançamento como um arquivo, e geralmente é uma falha de design que causa esse problema, portanto, corrigi-lo seria melhor do que lançar uma exceção.
Em seguida, vem a atualização de recursos. Este ponto, pelo menos para mim, está intimamente relacionado ao aspecto de operações críticas do aplicativo. Imagine uma
Employee
classe com uma funçãoUpdateDetails(std::string&)
que modifique os detalhes com base na sequência separada por vírgula fornecida. Semelhante à falha na liberação de memória, acho difícil imaginar a atribuição de valores de variáveis de membros com falha devido à minha falta de experiência em domínios onde esses podem ocorrer. No entanto,UpdateDetailsAndUpdateFile(std::string&)
espera-se que uma função como a que faz o nome indica falhe. Isso é o que chamo de operação crítica.Agora, você precisa verificar se a chamada operação crítica merece uma exceção. Quero dizer, a atualização do arquivo está acontecendo no final, como no destruidor, ou é simplesmente uma ligação paranóica feita após cada atualização? Existe um mecanismo de fallback que grava objetos não escritos regularmente? O que estou dizendo é que você deve avaliar a criticidade da operação.
Obviamente, existem muitas operações críticas que não estão vinculadas ao recurso. Se os
UpdateDetails()
dados forem fornecidos incorretamente, ele não atualizará os detalhes e a falha deverá ser divulgada; portanto, você lançaria uma exceção aqui. Mas imagine uma função comoGiveRaise()
. Agora, se o empregado mencionado tiver sorte de ter um chefe de cabelos pontudos e não conseguir um aumento (em termos de programação, o valor de alguma variável impede que isso aconteça), a função falhou essencialmente. Você lançaria uma exceção aqui? O que estou dizendo é que você precisa avaliar a necessidade de uma exceção.Para mim, a consistência é em termos da minha abordagem de design do que a usabilidade das minhas aulas. O que quero dizer é que não penso em termos de 'todas as funções Get devem fazer isso e todas as funções de atualização devem fazer isso', mas ver se uma função específica atrai uma certa idéia dentro da minha abordagem. Aparentemente, as aulas podem parecer meio 'aleatórias', mas sempre que os usuários (principalmente colegas de outras equipes) discordam ou perguntam sobre isso, eu explico e eles parecem satisfeitos.
Eu vejo muitas pessoas que basicamente substituem valores de retorno com exceções porque usam C ++ e não C, e isso oferece uma 'boa separação de tratamento de erros' etc. tais pessoas.
fonte
Em primeiro lugar, como já foi dito, as coisas não são que clara em C ++, IMHO principalmente porque os requisitos e restrições são um pouco mais variado em C ++ do que outras línguas, esp. C # e Java, que têm problemas de exceção "semelhantes".
Vou expor no exemplo std :: stof:
O contrato básico , a meu ver, dessa função é que ele tenta converter seu argumento em um float, e qualquer falha nesse procedimento é relatada por uma exceção. Ambas as exceções possíveis são derivadas,
logic_error
mas não no sentido de erro do programador, mas no sentido de "a entrada não pode, jamais, ser convertida em um float".Aqui, pode-se dizer que a
logic_error
é usada para indicar que, dada essa entrada (em tempo de execução), sempre é um erro tentar convertê-la - mas é o trabalho da função determinar isso e informar (por exceção).Nota lateral: Nessa visão, a
runtime_error
poderia ser visto como algo que, dada a mesma entrada para uma função, teoricamente poderia ter sucesso em execuções diferentes. (por exemplo, uma operação de arquivo, acesso ao banco de dados, etc.)Nota: A biblioteca de regex C ++ optou por derivar seu erro,
runtime_error
embora haja casos em que ela pode ser classificada da mesma forma que aqui (padrão de regex inválido).Isso apenas mostra, IMHO, que o agrupamento
logic_
ouruntime_
erro é bastante confuso em C ++ e não ajuda muito no caso geral (*) - se você precisar lidar com erros específicos, provavelmente precisará capturar menos do que os dois.(*): Isso não quer dizer que um único pedaço de código não deve ser consistente, mas se você joga
runtime_
oulogic_
oucustom_
algumas coisas que não é realmente importante, eu acho.Para comentar sobre ambos
stof
ebitset
:Ambas as funções recebem seqüências de caracteres como argumento e, nos dois casos, é:
Esta declaração tem, IMHO, duas raízes:
Desempenho : se uma função é chamada em um caminho crítico e o caso "excepcional" não é excepcional, ou seja, uma quantidade significativa de passes envolve o lançamento de uma exceção, e pagar todas as vezes pelo mecanismo de desenrolar exceção não faz sentido. e pode ser muito lento.
Localidade do tratamento de erros : se uma função é chamada e a exceção é capturada e processada imediatamente, há pouco sentido em lançar uma exceção, pois o tratamento de erros será mais detalhado com o
catch
que com umif
.Exemplo:
Aqui é onde funções como
TryParse
vs.Parse
entram em cena: Uma versão para quando o código local espera que a string analisada seja válida, uma versão quando o código local assume que é realmente esperado (ou seja, não excepcional) que a análise falhe.Na verdade,
stof
é apenas (definido como) um invólucrostrtof
, portanto, se você não deseja exceções, use esse.IMHO, você tem dois casos:
Função "Biblioteca" (reutilizada frequentemente em diferentes contextos): Basicamente, você não pode decidir. Possivelmente, forneça ambas as versões, talvez uma que relate um erro e uma versão que converta o erro retornado em uma exceção.
Função "Aplicativo" (específica para um blob de código do aplicativo, pode ser reutilizada em alguns casos, mas é limitada pelo estilo de tratamento de erros do aplicativo etc.): Aqui, geralmente deve ser bem claro. Se o (s) caminho (s) de código que chamam as funções lidam com exceções de maneira sã e útil, use as exceções para relatar qualquer erro (mas veja abaixo) . Se o código do aplicativo for mais facilmente lido e gravado para um estilo de retorno de erro, use-o de qualquer maneira.
É claro que haverá lugares no meio - apenas use o que precisa e lembre-se da YAGNI.
Por fim, acho que devo voltar à declaração de perguntas frequentes,
Eu assino isso para todos os erros que são uma indicação clara de que algo está gravemente bagunçado ou que o código de chamada claramente não sabia o que estava fazendo.
Mas quando isso é apropriado, muitas vezes é altamente específico do aplicativo, portanto, veja acima o domínio da biblioteca vs. o domínio do aplicativo.
Isso recai sobre a questão sobre se e como validar as pré - condições de chamada , mas não vou entrar nisso, responda já por muito tempo :-)
fonte