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_errormas 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_errorembora 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 stofe 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 catchque 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 TryParsevs. Parseentram 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 :-)