O problema:
Desde muito tempo, estou preocupado com o exceptionsmecanismo, porque sinto que ele realmente não resolve o que deveria.
REIVINDICAÇÃO: Há longos debates externos sobre esse tópico, e a maioria deles luta para comparar ou exceptionsretornar um código de erro. Definitivamente, este não é o tópico aqui.
Tentando definir um erro, eu concordo com o CppCoreGuidelines, da Bjarne Stroustrup & Herb Sutter
Um erro significa que a função não pode atingir seu objetivo anunciado
REIVINDICAÇÃO: O exceptionmecanismo é uma linguagem semântica para o tratamento de erros.
REIVINDICAÇÃO: Para mim, "não há desculpa" para uma função por não realizar uma tarefa: definimos condições pré / pós incorretamente para que a função não possa garantir resultados ou algum caso excepcional específico não é considerado importante o suficiente para gastar tempo no desenvolvimento uma solução. Considerando que, na IMO, a diferença entre o código normal e o tratamento do código de erro é (antes da implementação) uma linha muito subjetiva.
REIVINDICAÇÃO: Usar exceções para indicar quando uma condição anterior ou posterior não é mantida é outro objetivo do exceptionmecanismo, principalmente para fins de depuração. Não viso esse uso exceptionsaqui.
Em muitos livros, tutoriais e outras fontes, eles tendem a mostrar o tratamento de erros como uma ciência bastante objetiva, resolvida exceptionse você só precisa catchdeles para ter um software robusto, capaz de se recuperar de qualquer situação. Mas meus vários anos como desenvolvedor me fizeram ver o problema de uma abordagem diferente:
- Os programadores tendem a simplificar sua tarefa lançando exceções quando o caso específico parece muito raro para ser implementado com cuidado. Os casos típicos são: problemas de falta de memória, problemas de disco cheio, problemas de arquivos corrompidos etc. Isso pode ser suficiente, mas nem sempre é decidido do nível da arquitetura.
- Os programadores tendem a não ler atentamente a documentação sobre exceções nas bibliotecas e geralmente não estão cientes de quais e quando uma função é executada. Além disso, mesmo quando eles sabem, eles realmente não os gerenciam.
- Os programadores tendem a não capturar exceções com antecedência suficiente e, quando o fazem, é principalmente para registrar e lançar ainda mais. (consulte o primeiro ponto).
Isso tem duas consequências:
- Erros que ocorrem com frequência são detectados no início do desenvolvimento e depurados (o que é bom).
- Exceções raras não são gerenciadas e fazem o sistema travar (com uma boa mensagem de log) na casa do usuário. Algumas vezes o erro é relatado, ou nem mesmo.
Considerando que, na IMO, o principal objetivo de um mecanismo de erro deve ser:
- Tornar visível no código onde algum caso específico não é gerenciado.
- Comunique o tempo de execução do problema ao código relacionado (pelo menos ao chamador) quando essa situação ocorrer.
- Fornece mecanismos de recuperação
A principal falha da exceptionsemântica como mecanismo de tratamento de erros é a IMO: é fácil ver onde a throwestá no código-fonte, mas não é absolutamente evidente se uma função específica pode ser lançada observando a declaração. Isso traz todo o problema que apresentei acima.
O idioma não aplica e verifica o código de erro tão estritamente quanto em outros aspectos do idioma (por exemplo, tipos fortes de variáveis)
Uma tentativa de solução
Com a intenção de melhorar isso, desenvolvi um sistema muito simples de tratamento de erros, que tenta colocar o tratamento no mesmo nível de importância que o código normal.
A ideia é:
- Cada função (relevante) recebe uma referência a um
successobjeto muito leve e pode configurá-lo para um status de erro no caso. O objeto fica muito claro até que um erro no texto seja salvo. - Uma função é incentivada a ignorar sua tarefa se o objeto fornecido já contiver um erro.
- Um erro nunca deve ser substituído.
O design completo obviamente leva em consideração cada aspecto (cerca de 10 páginas) e também como aplicá-lo ao OOP.
Exemplo da Successclasse:
class Success
{
public:
enum SuccessStatus
{
ok = 0, // All is fine
error = 1, // Any error has been reached
uninitialized = 2, // Initialization is required
finished = 3, // This object already performed its task and is not useful anymore
unimplemented = 4, // This feature is not implemented already
};
Success(){}
Success( const Success& v);
virtual ~Success() = default;
virtual Success& operator= (const Success& v);
// Comparators
virtual bool operator==( const Success& s)const { return (this->status==s.status && this->stateStr==s.stateStr);}
virtual bool operator!=( const Success& s)const { return (this->status!=s.status || this->stateStr==s.stateStr);}
// Retrieve if the status is not "ok"
virtual bool operator!() const { return status!=ok;}
// Retrieve if the status is "ok"
operator bool() const { return status==ok;}
// Set a new status
virtual Success& set( SuccessStatus status, std::string msg="");
virtual void reset();
virtual std::string toString() const{ return stateStr;}
virtual SuccessStatus getStatus() const { return status; }
virtual operator SuccessStatus() const { return status; }
private:
std::string stateStr;
SuccessStatus status = Success::ok;
};
Uso:
double mySqrt( Success& s, double v)
{
double result = 0.0;
if (!s) ; // do nothing
else if (v<0.0) s.set(Error, "Square root require non-negative input.");
else result = std::sqrt(v);
return result;
}
Success s;
mySqrt(s, 144.0);
otherStuff(s);
saveStuff(s);
if (s) /*All is good*/;
else cout << s << endl;
Eu usei isso em muitos códigos (próprios) e forçou o programador (eu) a pensar mais sobre possíveis casos excepcionais e como resolvê-los (bom). No entanto, ele possui uma curva de aprendizado e não se integra bem ao código que agora o utiliza.
A questão
Eu gostaria de entender melhor as implicações do uso desse paradigma em um projeto:
- A premissa do problema está correta? ou Perdi algo relevante?
- A solução é uma boa idéia arquitetônica? ou o preço é muito alto?
EDITAR:
Comparação entre métodos:
//Exceptions:
// Incorrect
File f = open("text.txt"); // Could throw but nothing tell it! Will crash
save(f);
// Correct
File f;
try
{
f = open("text.txt");
save(f);
}
catch( ... )
{
// do something
}
//Error code (mixed):
// Incorrect
File f = open("text.txt"); //Nothing tell you it may fail! Will crash
save(f);
// Correct
File f = open("text.txt");
if (f) save(f);
//Error code (pure);
// Incorrect
File f;
open(f, "text.txt"); //Easy to forget the return value! will crash
save(f);
//Correct
File f;
Error er = open(f, "text.txt");
if (!er) save(f);
//Success mechanism:
Success s;
File f;
open(s, "text.txt");
save(s, f); //s cannot be avoided, will never crash.
if (s) ... //optional. If you created s, you probably don't forget it.