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:
passar uma string vazia para std :: stof (lançará invalid_argument), não um erro de programação
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_
ou runtime_
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_
ou logic_
ou custom_
algumas coisas que não é realmente importante, eu acho.
Para comentar sobre ambos stof
e bitset
:
Ambas as funções recebem seqüências de caracteres como argumento e, nos dois casos, é:
- não é trivial verificar se o chamador é válido ou não, se uma determinada string é válida (por exemplo, no pior caso, você teria que replicar a lógica da função; no caso de bitset, não está claro se a string vazia é válida; portanto, deixe o ctor decidir)
- Já é responsabilidade da função "analisar" a sequência, portanto, ela já tem que validá-la; portanto, faz sentido que reporte um erro ao "usar" a sequência de maneira uniforme (e nos dois casos isso é uma exceção) .
A 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?
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 um if
.
Exemplo:
float readOrDefault;
try {
readOrDefault = stof(...);
} catch(std::exception&) {
// discard execption, just use default value
readOrDefault = 3.14f; // 3.14 is the default value if cannot be read
}
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ólucro strtof
, portanto, se você não deseja exceções, use esse.
Então, como devo decidir se devo usar exceções ou não para uma função específica?
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,
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 travar o processo ...
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 :-)