C ++ parece preferir usar exceções com mais frequência.
Eu sugeriria, na verdade, menos do que o Objective-C em alguns aspectos, porque a biblioteca padrão C ++ geralmente não gerava erros de programador, como acesso fora dos limites de uma sequência de acesso aleatório em sua forma de design de caso mais comum (em operator[]
, por exemplo) ou tentando desreferenciar um iterador inválido. A linguagem não se preocupa em acessar uma matriz fora dos limites ou desreferenciar um ponteiro nulo ou qualquer coisa desse tipo.
Tirar os erros do programador em grande parte da equação de tratamento de exceções, na verdade, elimina uma categoria muito grande de erros aos quais outros idiomas costumam responder throwing
. O C ++ tende a assert
(que não é compilado nas compilações de lançamento / produção, apenas compilações de depuração) ou apenas falha (geralmente travando) nesses casos, provavelmente em parte porque a linguagem não deseja impor o custo dessas verificações de tempo de execução como seria necessário para detectar esses erros do programador, a menos que o programador queira especificamente pagar os custos, escrevendo o código que executa essas verificações ele mesmo.
Sutter ainda incentiva a evitar exceções em tais casos nos C ++ Coding Standards:
A principal desvantagem do uso de uma exceção para relatar um erro de programação é que você realmente não deseja que o desenrolamento da pilha ocorra quando deseja que o depurador seja iniciado na linha exata em que a violação foi detectada, com o estado da linha intacto. Em resumo: existem erros que você sabe que podem ocorrer (consulte os itens 69 a 75). Para todo o resto que não deveria, e é culpa do programador, se houver, existe assert
.
Essa regra não é necessariamente imutável. Em alguns casos mais críticos, pode ser preferível usar, por exemplo, wrappers e um padrão de codificação que registre uniformemente onde ocorrem erros do programador e throw
na presença de erros do programador, como tentar deferir algo inválido ou acessá-lo fora dos limites, porque pode ser muito caro não conseguir recuperar nesses casos, se o software tiver uma chance. Mas, em geral, o uso mais comum da linguagem tende a favorecer o não lançamento de erros de programadores.
Exceções externas
Onde eu vejo exceções incentivadas com mais freqüência em C ++ (de acordo com o comitê padrão, por exemplo) é para "exceções externas", como um resultado inesperado em alguma fonte externa fora do programa. Um exemplo está falhando ao alocar memória. Outro está falhando ao abrir um arquivo crítico necessário para a execução do software. Outro está falhando ao se conectar a um servidor necessário. Outro é um usuário pressionando um botão de cancelamento para cancelar uma operação cujo caminho de execução de caso comum espera obter êxito, sem essa interrupção externa. Todas essas coisas estão fora do controle do software imediato e dos programadores que o criaram. São resultados inesperados de fontes externas que impedem que a operação (que realmente deve ser considerada uma transação indivisível no meu livro *) seja bem-sucedida.
Transações
Eu geralmente incentivo a olhar para um try
bloco como uma "transação" porque as transações devem ter sucesso como um todo ou falhar como um todo. Se estivermos tentando fazer alguma coisa e ela falhar no meio do caminho, todos os efeitos colaterais / mutações feitos no estado do programa geralmente precisam ser revertidos para colocar o sistema novamente em um estado válido, como se a transação nunca tivesse sido executada, assim como um RDBMS que falha ao processar uma consulta no meio não deve comprometer a integridade do banco de dados. Se você alterar o estado do programa diretamente na transação, deverá "desativá-lo" ao encontrar um erro (e aqui os protetores de escopo podem ser úteis com o RAII).
A alternativa muito mais simples é não alterar o estado original do programa; você pode alterar uma cópia dela e, se for bem-sucedida, troque a cópia pelo original (garantindo que a troca não seja possível). Se falhar, descarte a cópia. Isso também se aplica mesmo se você não usar exceções para o tratamento de erros em geral. Uma mentalidade "transacional" é essencial para a recuperação adequada se ocorrerem mutações no estado do programa antes de encontrar um erro. Ou é bem-sucedido como um todo ou falha como um todo. Ele ainda não conseguiu fazer suas mutações.
Este é estranhamente um dos tópicos menos discutidos com frequência quando vejo programadores perguntando sobre como manipular corretamente erros ou exceções, mas é o mais difícil de todos eles acertar em qualquer software que queira alterar diretamente o estado do programa em muitos suas operações. Pureza e imutabilidade podem ajudar aqui a obter segurança de exceção tanto quanto ajudam na segurança de threads, pois um efeito colateral externo / mutação que não ocorre não precisa ser revertido.
atuação
Outro fator norteador no uso ou não de exceções é o desempenho, e não me refiro de uma maneira obsessiva, penny beliscante e contraproducente. Muitos compiladores C ++ implementam o que é chamado de "Tratamento de exceção de custo zero".
Ele oferece sobrecarga de tempo de execução zero para uma execução sem erros, que supera até a do tratamento de erros de valor de retorno C. Como compensação, a propagação de uma exceção tem uma grande sobrecarga.
De acordo com o que eu li sobre isso, faz com que os caminhos de execução de caso comuns não exijam sobrecarga (nem mesmo a sobrecarga que normalmente acompanha a manipulação e propagação do código de erro no estilo C), em troca de distorcer os custos em direção aos caminhos excepcionais ( throwing
agora significa mais caro do que nunca).
"Caro" é um pouco difícil de quantificar, mas, para começar, você provavelmente não quer jogar um milhão de vezes em um loop apertado. Esse tipo de design pressupõe que as exceções não ocorram esquerda e direita o tempo todo.
Não erros
E esse ponto de desempenho me leva a não erros, o que é surpreendentemente impreciso se olharmos para todos os tipos de outros idiomas. Mas eu diria que, dado o design de EH de custo zero mencionado acima, você quase certamente não deseja throw
em resposta a uma chave que não foi encontrada em um conjunto. Porque isso não é apenas indiscutivelmente um erro (a pessoa que está pesquisando a chave pode ter construído o conjunto e espera estar pesquisando por chaves que nem sempre existem), mas seria muito caro nesse contexto.
Por exemplo, uma função de interseção de conjunto pode querer percorrer dois conjuntos e procurar as chaves que eles têm em comum. Se não conseguir encontrar uma chave threw
, você percorrerá e poderá encontrar exceções em metade ou mais das iterações:
Set<int> set_intersection(const Set<int>& a, const Set<int>& b)
{
Set<int> intersection;
for (int key: a)
{
try
{
b.find(key);
intersection.insert(other_key);
}
catch (const KeyNotFoundException&)
{
// Do nothing.
}
}
return intersection;
}
O exemplo acima é absolutamente ridículo e exagerado, mas vi no código de produção algumas pessoas provenientes de outras linguagens que usam exceções em C ++, mais ou menos assim, e acho que é uma afirmação razoavelmente prática de que esse não é um uso apropriado de exceções em C ++. Outra dica acima é que você notará que o catch
bloco não tem absolutamente nada a ver e foi escrito apenas para ignorar forçosamente essas exceções, e isso geralmente é uma dica (embora não seja uma garantia) de que as exceções provavelmente não estão sendo usadas de maneira apropriada no C ++.
Para esses tipos de casos, algum tipo de valor de retorno indicando falha (qualquer coisa, desde retornar false
a um iterador inválido nullptr
ou qualquer outra coisa que faça sentido no contexto) é geralmente muito mais apropriado e também frequentemente mais prático e produtivo, pois um tipo de erro sem erro O caso geralmente não exige que algum processo de desenrolamento de pilha chegue ao catch
site analógico .
Questões
Eu teria que ir com sinalizadores de erro interno se optar por evitar exceções. Será muito difícil lidar com isso ou talvez funcione ainda melhor do que exceções? Uma comparação dos dois casos seria a melhor resposta.
Evitar exceções imediatas em C ++ parece extremamente contraproducente para mim, a menos que você esteja trabalhando em algum sistema incorporado ou em um tipo específico de caso que proíba o uso deles (nesse caso, você também teria que se esforçar para evitar tudo biblioteca e idioma que, de outra forma throw
, gostariam de usar estritamente nothrow
new
).
Se você absolutamente precisa evitar exceções por qualquer motivo (por exemplo, trabalhando nos limites da API C de um módulo cuja API C você exporta), muitos podem discordar de mim, mas eu sugiro usar um manipulador / status global de erros como o OpenGL glGetError()
. Você pode fazê-lo usar o armazenamento local do encadeamento para ter um status de erro exclusivo por encadeamento.
Minha justificativa é que não estou acostumado a ver equipes em ambientes de produção verificar minuciosamente todos os erros possíveis, infelizmente, quando códigos de erro são retornados. Se eles foram completos, algumas APIs C podem encontrar um erro em praticamente todas as chamadas da API C, e a verificação completa exigiria algo como:
if ((err = ApiCall(...)) != success)
{
// Handle error
}
... com quase todas as linhas de código que invocam a API que exige essas verificações. Ainda não tive a sorte de trabalhar com equipes tão completas. Eles geralmente ignoram esses erros na metade, às vezes até na maior parte do tempo. Esse é o maior apelo para mim das exceções. Se envolvermos essa API e a fizermos uniformemente throw
ao encontrar um erro, a exceção não poderá ser ignorada e, na minha opinião, e experiência, é aí que reside a superioridade das exceções.
Mas se as exceções não puderem ser usadas, o status de erro global por thread, pelo menos, tem a vantagem (enorme em comparação com o retorno de códigos de erro para mim) de que ele pode ter a chance de detectar um erro anterior um pouco mais tarde do que quando ocorreu em alguma base de código desleixada, em vez de perdê-la completamente e nos deixar completamente alheios ao que aconteceu. O erro pode ter ocorrido algumas linhas antes ou em uma chamada de função anterior, mas, desde que o software ainda não tenha travado, poderemos começar a trabalhar de trás para frente e descobrir onde e por que ocorreu.
Parece-me que, como os ponteiros são raros, eu precisaria usar sinalizadores de erro interno se optar por evitar exceções.
Eu não diria necessariamente que indicadores são raros. Atualmente, existem métodos no C ++ 11 e posteriores para obter os ponteiros de dados subjacentes dos contêineres e uma nova nullptr
palavra-chave. É geralmente considerado imprudente usar ponteiros brutos para possuir / gerenciar memória, se você puder usar algo parecido, unique_ptr
considerando o quão crítico é estar em conformidade com RAII na presença de exceções. Mas indicadores brutos que não possuem / gerenciam memória não são necessariamente considerados tão ruins (mesmo de pessoas como Sutter e Stroustrup) e, às vezes, muito práticos como uma maneira de apontar para as coisas (junto com índices que apontam para as coisas).
Eles são indiscutivelmente menos seguros que os iteradores de contêiner padrão (pelo menos na versão, iteradores verificados ausentes) que não serão detectados se você tentar desreferenciá-los depois que eles forem invalidados. O C ++ ainda é, sem vergonha, uma linguagem perigosa, eu diria, a menos que seu uso específico queira agrupar tudo e ocultar até mesmo ponteiros brutos que não sejam proprietários. É quase crítico, com exceções, que os recursos estejam em conformidade com o RAII (que geralmente não tem custo de tempo de execução), mas, além disso, não está necessariamente tentando ser a linguagem mais segura para evitar custos que um desenvolvedor não deseja explicitamente. trocar por outra coisa. O uso recomendado não está tentando protegê-lo de coisas como ponteiros pendurados e iteradores invalidados, por assim dizer (caso contrário, seremos incentivados a usarshared_ptr
em todo o lugar, que Stroustrup se opõe veementemente). Ele está tentando protegê-lo de deixar de liberar / liberar / destruir / desbloquear / limpar adequadamente um recurso quando algo acontecer throws
.