Ponto V!

Home C/C++ Conceitos Básicos Exceções: Guia de Uso
Vinícius Godoy de Mendonça
Exceções: Guia de UsoImprimir
Escrito por Vinícius Godoy de Mendonça

No artigo anterior, vimos o poderoso mecanismo de tratamento de erros do C++, as exceções. Entretanto, falamos pouco de como exceções são usadas no dia-a-dia. Quando é desejável capturar exceções? E dispará-las?

Nesse artigo, discutiremos como usar com efetividade as exceções, e o que deve ser evitado ao lidar com esse poderoso mecanismo.

Lançando exceções

Quando construímos uma engine, ou escrevemos um código, temos a necessidade de reportar os erros que ocorrem. Nessa hora, deveremos nos preocupar em lançar exceções para outros programadores, usuários de nossas classes, capturarem. Vejamos algumas recomendações importantes nesse sentido.

Dê um nome descritivo

O próprio nome da classe da exceção deve dar uma visão clara sobre que erro está sendo reportado. É uma prática comum terminar a classe com o nome “Exception” ou “Error”.

No C++, não há uma definição formal entre as duas palavras, entretanto, outras linguagens como o Java e o C# adotam a seguinte convenção: o termo “Error” se refere a problemas do qual a aplicação dificilmente poderá se recuperar, enquanto Exception são para situações de aviso. Esta é de fato uma boa convenção, e já tenho visto programas em C++ adotando esse padrão.

Alguns exemplos de nome:“IOException”, “FileClosedException”, “MissingDLLError", “OutOfMemoryError”, etc.

Dê uma boa descrição

Além do nome, boas exceções contém um texto descritivo sobre o erro. A menos que seja inevitável, evite simplesmente repetir de forma discursiva o nome do erro. O ideal é fornecer mais informações. Por exemplo, uma função receba um ano entre 95 e hoje, poderia disparar uma IllegalArgumentException contendo a seguinte mensagem, para uma data de 1970: “Argumento inválido: O ano deve estar entre 1995 e o atual. Informado: 1970”.

O texto da descrição deve ser conciso e normalmente será escrito para outro programador, e não para o usuário final. Antes de querer aproveitar o embalo e exibir exceções para seu usuário, lembre-se que bons programas geralmente são feitos com internacionalização em mente, e isso geralmente será controlado apenas em camadas superiores da aplicação (as chamadas “camadas de visão”).

Escolha bem a classe da exceção

Embora seja possível criar todas as suas exceções como “std::exception”, não faça isso. Planeje uma hierarquia de classes consistente para suas exceções.

Dispare erros específicos, que podem ser tratados facilmente num bloco catch, sem a necessidade de ifs, switchs ou análise do texto da mensagem. Entretanto, planeje um conjunto de superclasses, de modo que você não obrigue seu usuário a tratar cada erro minuciosamente.

Sua obrigação é ser descritivo, mas quem escolhe o quanto de descrição precisa num determinado momento, é o usuário de suas classes. Veja como exemplo a árvore de exceções de uma engine hipotética:

Árvore de exceções da engine

Logicamente, podemos esperar que existam outras classes abaixo de NetworkException e GraphicsException, não listadas nesse diagrama. Mas é importante notar que temos uma visão clara do que essas classes seriam, mesmo que elas não apareçam aqui.

Outro detalhe importante é a criação de uma superclasse principal para toda sua biblioteca, nesse caso, engine::Exception. Assim, fica fácil para quem utiliza-la separar o registro de erros de sua biblioteca num arquivo específico, por exemplo.

Anexe outras informações pertinentes

Outras informações comumente encontradas em exceções são:

  • Local onde a exceção ocorreu: Que pode ser obtido com os macros __FILE__ e __LINE__, no momento da criação da exceção (em linguagens como Java e C#, toda pilha de execução é anexada automaticamente à exceção);
  • Outras exceções: Uma exceção pode ter sido gerada em decorrência de outra, nesse caso, é interessante incluir a geradora como um dos atributos da exception disparada;
  • Origem: Em alguns casos pode ser interessante incluir o objeto que originou a exceção. Essa informação é especialmente relevante quando a exceção é disparada por um objeto que está numa lista de objetos, e a lista está encaminhando a exceção para fora de si, ao invés de tratá-la;

Dispare exceções relevantes

Muitas vezes, seu código poderá executar funções que disparam, elas mesmas, suas próprias exceções. Nesse caso, há algumas formas que você deve proceder:

Se você puder tratar a exceção completamente, não redispare a exceção. Isso permite que métodos poderosos possam gerenciar uma série de eventos, antes de uma exceção pipocar para seus usuários. Um exemplo disso poderia ser um método que leia uma valor de um XML, e usa um valor padrão caso esse valor não exista:

std::string Enemy::readProperty(const std::string& property, const std::string defaultValue) 
{
    try 
    {
        return xml.readProperty(“/enemy/properties/”, property);
    } 
    catch (XMLPropertyNotFoundException) 
    {
        return defaultValue;
    } 
}

Se você não puder tratar a exceção, certifique-se de encaminhar apenas exceções relevantes a sua camada de abstração. Por exemplo, no método acima, seria relevante disparar outras XMLExceptions?

Normalmente, quem usa a classe Enemy não deveria ter a menor ideia de onde Enemy salva seus dados. Assim, os programadores da classe Enemy poderiam mudar de planos no futuro e usar um arquivo binário, ou um banco de dados para salvar os dados no futuro, sem que o programa todo seja alterado.

Como fazer então para disparar a exceção? Nesse caso, você deverá disparar uma exceção própria e colocar a exceção original como um de seus parâmetros:

std::string Enemy::readProperty(const std::string& property, const std::string defaultValue) 
{
    try 
    {
        return xml.readProperty(“/enemy/properties/”, property);
    } 
    catch (XMLPropertyNotFoundException) 
    {
        return defaultValue;
    } 
    catch (XMLException e)
    {
        throw CorruptedPropertyException(
           std::string(“Unable to read property:”) + property, e);
    }
}

Não despreze alternativas convenientes

Nem todos os métodos são críticos, e precisam de detalhes sobre erros. Alguns métodos, como o readProperty anterior podem retornar valores especiais no caso de erros. Em outros métodos, a informação específica do erro normalmente é tão irrelevante, que um retorno na forma de um boolean pode ser conveniente.

Quando você identificar um caso assim, você poderá fornecer duas interfaces para seu método uma com e outra sem a exceção. Por exemplo, um método que transforma uma string num número, poderia ser feito assim:

bool tryParseInt(const std::string& texto, int& result) 
{
    try
    {
        result = parseInt(texto);
        return true;
    } 
    catch (…)
    {
        return false;
    }
}

O método tryParseInt é a alternativa conveniente para quem não está muito interessado nas razões do erro. Um exemplo disso são pessoas que estão usando o método para converter dados digitados pelo usuário. No caso de falhas, ele simplesmente requisitará o dado novamente.

Note, entretanto, que o método parseInt ainda existe. Estamos oferecendo apenas alternativas. A regra de que é o usuário do seu método que escolhe o quanto de detalhamento de erro ele quer ainda vale.

Logicamente, você poderia fazer a implementação acima ao contrário: programar o método tryParseInt primeiro, e programar o método parseInt de modo a chamar o tryParse e disparar a exceção caso a operação falhe. Isso não importa, o importante é manter as duas opções.

Entretanto, se os valores retornados por seu método alternativo não forem triviais, e começarem a se parecer com códigos de erro, volte atrás em sua decisão e deixe apenas o método que dispara a exceção disponível.

Valide parâmetros de entrada

Especialmente em sistemas comerciais ou naqueles trechos em que os dados vem de um usuário ou sistema externo, use exceções para validar cuidadosamente os dados de entrada de seus métodos. Não delegue essa tarefa para métodos que você chama, pois estes poderão ser modificados por outro programador, ou por você mesmo, no futuro. Veja por exemplo o trecho de código abaixo:

//Sets an age between 12 and 100 years old.
void User::setAge(int age) 
{
    this->age = age;
}

Esse método está pedindo a idade do usuário. Mas será que usuários de todas as idades poderiam usar o software? A documentação diz claramente que não.

Vamos melhorar o método reforçando essas regras:

//Sets an age between 12 and 100 years old.
void user::setAge(int age) 
{
    if (age < 12 || age > 100)   
       throw InvalidAgeException(“Invalid age. Should be 12 and 100.”);
    this->age = age;
}

Note que a exceção foi disparada logo no início do método, assim que o problema foi detectado. Essa prática é chamada de code guard. Como a exceção interrompe o fluxo, não precisamos de else – o que evita endentação desnecessária, aumentando a legibilidade do código.

Uma dica importante: Melhor do que validar dados através de exceções é, quando possível, evitar através de tipos de dados. Por exemplo, digamos que você tenha o seguinte método, que define a arma do personagem em seu shooter, e que uma arma (nem que seja a faca) seja obrigatória:

void Character::setWeapon(Weapon* weapon) 
{
    if (weapon == NULL)
        throw new InvalidWeaponException(“Weapon cannot be null!”);

    this->weapon = *weapon;
}

Essa exceção poderia ser totalmente evitada, se o parâmetro fosse passado por referência.

void Character::setWeapon(Weapon& weapon) 
{
    this->weapon = weapon;
}

Não só o código fica mais legível, como uma tentativa de definir NULL para o método gerará um erro em tempo de compilação.

Observação: Algumas pessoas não validam parâmetros de entrada alegando que querem aumentar o desempenho de seu programa. Embora essa justificativa possa ser válida em alguns casos, certifique-se que você está realmente num trecho de performance crítica do seu programa, antes de abrir mão da segurança que essa dica trará.

Capturando exceções

Agora que já dominamos a arte de lançar exceções, que tal entendermos as boas práticas sobre como capturá-las?

Capture exceções através de referências

Quando você utiliza hierarquias de classes em suas exceções, é importante lembrar que no C++, polimorfismo funciona apenas através de ponteiros e referências. Quando a atribuição é feita por valor, você está dizendo ao C++ que você quer que o pai receba uma cópia do conteúdo do filho. Veja um exemplo:

Pai &pai = filho; //Ok. A referência do pai pode apontar para o filho
Pai *pai = *filho; //Ok. Ponteiro do pai com o endereço do filho
Pai pai = filho; //Ops...Queremos que o pai seja uma cópia do filho?

A última chamada irá disparar o operador de = para que a cópia seja feita. E o que acontecerá com todos os atributos que o filho tem a mais do que o pai? Eles simplesmente não são copiados.

E o que acontece com os métodos virtuais? Bem, são os métodos do pai que passam a ser chamados, não do filho. Afinal, você agora está trabalhando com um objeto da classe pai, que tem atributos iguais aos objetos da classe filho.

As regras não mudam quando capturamos exceções. Portanto, um try nunca deverá esperar uma exceção pai por valor. Isso causará o problema conhecido como “exception slicing”, ou seja, sua exceção será copiada para um do tipo pai, e todos os atributos adicionais ou sobrecargas de método (como o what()) se perderão no processo. Além disso, a cópia por valor ainda tem um custo adicional de desempenho, já que copiar todos os dados da exceção é muito mais caro do que simplesmente seu endereço de memória.

Vamos reforçar com um exemplo:

try
{
    //Código que pode dar erro
} 
catch (ExcecaoPai &e) //Correto, por referência
{
}
catch (ExcecaoPai2 e) // ERRADO! SEM OPERADOR DE & SERÁ FATIADA!
{
}

Não engula exceções

É isso mesmo. Muitos programadores, ao perceber que os métodos que usam ocasionalmente disparam exceções, fazem o seguinte código:

try 
{
    codigoQueDisparaExcecoes();
} 
catch (...) {}

Essa técnica tem um nome: POG. É a forma mais prática para boicotar a si mesmo. Fazendo isso, você perde a informação do erro e torna o seu próprio trabalho muito mais difícil. Se não sabe como tratar exceções, releia as dicas do tópico “Dispare exceções relevantes”.

O que fazer caso aquelas dicas não sirvam, e realmente não haja nada o que se possa fazer com a exceção? Isso nos leva ao nosso próximo item.

Tenha um mecanismo de registro de exceções

Exato, o famoso log. Se a exceção subir, subir, subir, ao ponto de não termos mais onde e nem como captura-la, não nos resta nada a fazer a não ser registrá-la em algum arquivo e abortar o programa.

No caso de um game, esse mecanismo poderia estar, por exemplo, no próprio game loop.

int main(int argc, char* argv) 
{
   try 
   {
       initStuff();
       doGameLoop(); //Run until the game ends
   } 
   catch (EngineException &e) 
   {
      logExceptionInFile(e);
   } 
   catch (std::exception &e) {
      logStandardExceptions(e);
   } 
   catch (...) 
   {
      log(“An error ocurred while running the game”);
   }
   deInitStuff();
}

Observe a preocupação em registrar as exceções com o maior grau de detalhamento possível. Caso seja uma EngineException, iremos registrar qual arquivo deu erro, em que linha, exceções causadoras, e todas as outras informações interessantes que inserimos em nosso motor de jogo nos itens “Dê uma boa descrição” e “Anexe informações relevantes”. Um exemplo de formato para esse registro poderia gerar é:

2012-01-29 21:49 ERROR: Unable to read property “life” at Enemy.cpp (144). Caused by – Incorrect XML format. Tag “life not found”

Outro detalhe importante sobre o tratamento que fizemos no main é notar que se uma std::exception for disparada, o método ainda poderá capturar o que estiver escrito no what() – e vamos torcer para que seja algo que nos ajude a diagnosticar o erro. Perceba aqui o quão importante é seguir a dica do artigo anterior, quando dissemos que era desejável derivar nossas exceções de std::exception. Esse game loop poderia ser do usuário de nossa engine e, talvez, seja esse o único catch que irá capturar nossa exceção, que, felizmente terá uma “boa descrição”.

Finalmente, há a última mensagem de erro, que certamente nos levará a frustração caso ocorra, pois não dá dica nenhuma de que problema ocorreu. O motivo de essa mensagem estar lá é só nos avisar que em algum canto de seu código existem exceções que não são descritivas e foram capturadas. Ao mesmo tempo, impedir que a exceção aborte o programa sem executar o código de finalização do jogo.

Não registre o que você tratou, ou que irá encaminhar

Alguns programadores são tão preocupados em capturar erros, que acabam seguindo o caminho oposto do que descrevemos acima. Ao invés de engolir exceções, eles registram exceções demais. Isso também é um problema, pois polui seu arquivo de log, tornando os erros de verdade difíceis de encontrar.

Se você capturou uma exceção num catch, e conseguiu tratá-la não registre-a no log. Se não conseguiu tratá-la, e irá dispará-la novamente, também não registre-a (afinal, talvez alguém consiga tratá-la adequadamente).

Quando a problema deve ir para o log, então? Você só deve registrar exceções caso realmente não tenha tratamento para elas, ou caso o tratamento tenha gerado algum tipo de efeito colateral que valha a pena ser reportado.

Cuidado com exceções em loops

Muitas vezes, o código que dispara exceções pode estar dentro de um loop. O que fazer nesse caso?

Devemos analisar se a exceção deve realmente abandonar o loop, ou se ela invalida apenas aquela iteração específica.

Por exemplo:

SocketController::processMessages()
{
    while (true) 
    {
        msg = socket.waitMessage();
        try 
        {
            parseMessage(msg);
        } 
        catch (ParseException &e) 
        {
            log(“Unable to parse message”, e);
        }
    }
}

Outra alternativa, seria incluir todas as exceções ocorridas numa lista, e dispará-las como atributo de uma outra exceção mais genérica (e essa sim, abandonar o loop):

SocketController::processMessages()
{
    std::vector errors;
    while (true) 
    {
        msg = socket.waitMessage();
        try 
        {
            parseMessage(msg);
        } 
        catch (ParseException &e) 
        {
            errors.push_back(e);
            if (errors.size() == 10) 
               throw CommunicationException(“Bad client.”, errors);
        }
    }
}

Finalizando

Nesse artigo, vimos dicas valiosas de como tratar erros na prática. Um mecanismo robusto de tratamento ajuda o programador a tornar seu código mais seguro e fácil de manter. Além disso, descrever bem exceções torna o trabalho de encontrar os erros consideravelmente mais simples, especialmente se esses erros ocorrerem numa máquina distante, como a do cliente.

Vimos que erros podem ocorrer em qualquer lugar, e precisamos estar preparados para identificá-los, tratá-los ou registrá-los adequadamente. Entretanto, o que fazer com arquivos abertos, ou objetos criados com new quando uma exceção é disparada? Como garantir que nosso programa liberará corretamente esses recursos? Há formas melhores do que sempre capturar exceções no catch e redispará-las? A resposta é sim.

Ficou curioso para saber como? Bem, esse será o tema de nosso próximo artigo. Até lá.


Comentários (9)
  • Neto  - Ponteiros mais rapidos que referencias ?
    avatar

    Observação: Algumas pessoas não fazem isso alegando que querem aumentar o desempenho de seu programa. Embora essa justificativa possa ser válida em alguns casos, certifique-se que você está realmente num trecho de performance crítica do seu programa, antes de abrir mão da segurança que essa dica trará.

    Como isso pode ser possível ? Desde quando ponteiros são mais rapidos que referencias ?

  • Vinícius Godoy de Mendonça
    avatar

    Não estava me referindo a ponteiros serem mais rápidos que referências.

    Estava me referindo a não validar os parâmetros de entrada em prol da performance. É a alegação usada na STL e em diversos frameworks gráficos.

    Mas, relendo ali o trecho, ficou mesmo esquisito, vou corrigir a frase. Obrigado pelo comentário.

  • Neto  - Hum...
    avatar

    Agora faz sentido... rsrsrsr... :D

  • Neto  - Artigo
    avatar

    Quando é que vai sair o outro artigo ? Esperando anciosamente!

  • Vinícius Godoy de Mendonça  - Novo artigo
    avatar

    Oi. Deve sair logo (no máximo mês que vem). E o tema será Gerência de Recursos.

  • Neto  - Maravilha!
    avatar

    Que beleza, fico no aguardo então ^^!

  • Jony  - Reconhecimento de Gestos
    avatar

    Bom dia Vinícius...

    no link http://www.guj.com.br/java/266586-programacao-para-kinect, que tem "montada uma API que integra a OpenNI ao OpenCV, escrita em C++ para reconhecimento de sinais da Lingua de Sinais Brasileira".

    Estou trabalhando neste projeto porém estou já levantei todos os requisitos necessários e agora irei passar para a fase de desenvolvimento.

    Seria possível me repassar algum material sobre esta API para que eu possa tomar por base?

    Obrigado

  • Vinícius Godoy de Mendonça
    avatar

    Oi, no próprio tópico tem o link para a lib que integra OpenNI + OpenCV que criei, chamada xncv: https://github.com/ViniGodoy/xncv

    Há exemplos de uso na pasta samples.

  • Jony  - Reconhecimento de Gestos
    avatar

    Boa noite Vinícius,

    obrigado pela ajuda. Analisarei os códigos e caso haja dúvidas, manterei contato... se assim me permitir.

    Muito obrigado.

Escrever um comentário
Your Contact Details:
Gravatar enabled
Comentário:
[b] [i] [u] [url] [quote] [code] [img]   
:angry::0:confused::cheer:B):evil::silly::dry::lol::kiss::D:pinch::(:shock:
:X:side::):P:unsure::woohoo::huh::whistle:;):S:!::?::idea::arrow:
Security
Por favor coloque o código anti-spam que você lê na imagem.
LAST_UPDATED2  

Busca

Linguagens

Twitter