O tratamento de exceções (EH) parece ser o padrão atual e, pesquisando na Web, não consigo encontrar novas idéias ou métodos que tentem melhorar ou substituí-lo (bem, existem algumas variações, mas nada de novo).
Embora a maioria das pessoas pareça ignorá-lo ou simplesmente aceitá-lo, o EH tem algumas desvantagens enormes: as exceções são invisíveis para o código e ele cria muitos e muitos pontos de saída possíveis. Joel on software escreveu um artigo sobre isso . A comparação com os goto
ajustes perfeitos, me fez pensar novamente sobre EH.
Tento evitar o EH e apenas uso valores de retorno, retornos de chamada ou o que for mais adequado ao objetivo. Mas quando você precisa escrever um código confiável, não pode ignorar o EH atualmente : ele começa com o new
, que pode gerar uma exceção, em vez de retornar 0 (como nos velhos tempos). Isso torna qualquer linha de código C ++ vulnerável a uma exceção. E mais lugares no código fundamental do C ++ lançam exceções ... std lib faz isso e assim por diante.
É como andar em terrenos instáveis . Então, agora somos forçados a cuidar de exceções!
Mas é difícil, é realmente difícil. Você precisa aprender a escrever um código de exceção seguro e, mesmo que tenha alguma experiência com ele, ainda será necessário verificar novamente qualquer linha de código para ser segura! Ou você começa a colocar blocos try / catch em todos os lugares, o que confunde o código até que ele atinja um estado de ilegibilidade.
O EH substituiu a antiga abordagem determinística limpa (valores de retorno ..), que tinha apenas algumas desvantagens, mas compreensíveis e facilmente solucionáveis, por uma abordagem que cria muitos pontos de saída possíveis no seu código e se você começar a escrever código que capte exceções (o que você são obrigados a fazê-lo em algum momento), ele até cria uma infinidade de caminhos através do seu código (código nos blocos de captura, pense em um programa de servidor no qual você precisa de recursos de registro diferentes de std :: cerr ..). EH tem vantagens, mas esse não é o ponto.
Minhas perguntas reais:
- Você realmente escreve código de exceção seguro?
- Tem certeza de que seu último código "pronto para produção" é seguro contra exceções?
- Você pode ter certeza disso?
- Você conhece e / ou realmente usa alternativas que funcionam?
fonte
Respostas:
Sua pergunta faz uma afirmação de que "Escrever código com exceção de segurança é muito difícil". Vou responder às suas perguntas primeiro e depois responder à pergunta oculta por trás delas.
Respondendo a perguntas
Claro que eu faço.
Esta é a razão pela qual o Java perdeu muito de seu apelo para mim como programador C ++ (falta de semântica RAII), mas estou discursando: Esta é uma pergunta do C ++.
Na verdade, é necessário quando você precisa trabalhar com o código STL ou Boost. Por exemplo, os threads C ++ (
boost::thread
oustd::thread
) lançam uma exceção para sair normalmente.Escrever código com exceção de segurança é como escrever código sem erros.
Você não pode ter 100% de certeza de que seu código é seguro contra exceções. Mas então, você se esforça para isso, usando padrões conhecidos e evitando anti-padrões conhecidos.
Não há alternativas viáveis no C ++ (ou seja, você precisará voltar ao C e evitar as bibliotecas C ++, além de surpresas externas como o Windows SEH).
Escrevendo código de exceção seguro
Para escrever um código de exceção seguro, você deve primeiro saber qual o nível de segurança de exceção de cada instrução que você escreve.
Por exemplo, um
new
pode lançar uma exceção, mas a atribuição de um built-in (por exemplo, um int ou um ponteiro) não falhará. Uma troca nunca falha (nunca escreva uma troca jogando), umstd::list::push_back
pode lançar ...Garantia de exceção
A primeira coisa a entender é que você deve poder avaliar a garantia de exceção oferecida por todas as suas funções:
Exemplo de código
O código a seguir parece o C ++ correto, mas, na verdade, oferece a garantia "none" e, portanto, não está correto:
Eu escrevo todo o meu código com esse tipo de análise em mente.
A garantia mais baixa oferecida é básica, mas a ordem de cada instrução torna a função inteira "nenhuma", porque se 3. lançar, x vazará.
A primeira coisa a fazer seria tornar a função "básica", ou seja, colocar x em um ponteiro inteligente até que ele pertença à lista com segurança:
Agora, nosso código oferece uma garantia "básica". Nada vazará e todos os objetos estarão em um estado correto. Mas poderíamos oferecer mais, isto é, a forte garantia. É aqui que pode se tornar caro, e é por isso que nem todo código C ++ é forte. Vamos tentar:
Reordenamos as operações, primeiro criando e configurando
X
seu valor correto. Se alguma operação falhar, elat
não será modificada e, portanto, as operações 1 a 3 poderão ser consideradas "fortes": se algo for lançado,t
não for modificado eX
não vazar porque é de propriedade do ponteiro inteligente.Então, criamos uma cópia
t2
det
, e trabalhar sobre esta cópia de operação de 4 a 7. Se algo joga,t2
é modificado, mas, em seguida,t
ainda é o original. Ainda oferecemos a garantia forte.Então, trocamos
t
et2
. As operações de troca devem ser notowow em C ++, então, esperamos que a troca para a qual você escreveu nãoT
seja notowow (se não for, reescreva-a para que não sejaowow).Portanto, se chegarmos ao final da função, tudo será bem-sucedido (Não há necessidade de um tipo de retorno) e
t
terá seu valor excedido. Se falhar,t
ainda terá seu valor original.Agora, oferecer a garantia forte pode ser bastante oneroso, portanto, não se esforce para oferecer a garantia forte a todo o seu código, mas se você puder fazê-lo sem nenhum custo (o inline C ++ e outras otimizações podem tornar todo o código acima sem custo) , então faça. O usuário da função agradecerá por isso.
Conclusão
É necessário algum hábito para escrever código com exceção de segurança. Você precisará avaliar a garantia oferecida por cada instrução que usar e, em seguida, avaliar a garantia oferecida por uma lista de instruções.
Obviamente, o compilador C ++ não fará backup da garantia (no meu código, ofereço a garantia como uma tag dowargen @warning), o que é meio triste, mas não deve impedi-lo de tentar escrever código com exceção de segurança.
Falha normal vs. erro
Como um programador pode garantir que uma função sem falha sempre terá êxito? Afinal, a função pode ter um bug.
Isso é verdade. As garantias de exceção devem ser oferecidas por código sem erros. Mas então, em qualquer idioma, chamar uma função supõe que a função esteja livre de erros. Nenhum código são se protege contra a possibilidade de haver um bug. Escreva o código da melhor maneira possível e, em seguida, ofereça a garantia com a suposição de que está livre de erros. E se houver um erro, corrija-o.
As exceções são para falhas excepcionais de processamento, não para erros de código.
Últimas palavras
Agora, a pergunta é "Isso vale a pena?".
Claro que é. Ter uma função "não mostrar / não falhar" sabendo que a função não falhará é um grande benefício. O mesmo pode ser dito para uma função "forte", que permite escrever código com semântica transacional, como bancos de dados, com recursos de confirmação / reversão, sendo a confirmação a execução normal do código, lançando exceções como a reversão.
Então, o "básico" é a garantia mínima que você deve oferecer. O C ++ é uma linguagem muito forte lá, com seus escopos, permitindo evitar vazamentos de recursos (algo que um coletor de lixo acharia difícil oferecer para o banco de dados, conexão ou identificadores de arquivo).
Assim, tanto quanto eu vê-lo, é pena.
Edit 29-01-2010: Sobre a troca não lançada
nobar fez um comentário que acredito ser bastante relevante, porque faz parte de "como você escreve um código de exceção seguro":
swap()
funções personalizadas por escrito . Note-se, no entanto, questd::swap()
pode falhar com base nas operações que utiliza internamenteo padrão
std::swap
fará cópias e atribuições que, para alguns objetos, podem ser lançadas. Assim, a troca padrão poderia ser lançada, usada para suas classes ou mesmo para classes STL. No que diz respeito ao padrão C ++, a operação de troca paravector
,deque
elist
não será lançada, ao contrário do que poderia acontecermap
se o functor de comparação pudesse lançar na construção de cópias (consulte A linguagem de programação C ++, edição especial, apêndice E, E.4.3 .Swap ).Observando a implementação do Visual C ++ 2008 da troca do vetor, a troca do vetor não será lançada se os dois vetores tiverem o mesmo alocador (por exemplo, o caso normal), mas fará cópias se eles tiverem alocadores diferentes. E, portanto, suponho que poderia ser lançado neste último caso.
Portanto, o texto original ainda é válido: nunca escreva uma troca de lançamento, mas o comentário de nobar deve ser lembrado: verifique se os objetos que você está trocando têm uma troca não de lançamento.
Edit 06-11-2011: artigo interessante
Dave Abrahams , que nos deu as garantias básicas / fortes / de não comparência , descreveu em um artigo sua experiência em tornar a exceção do STL segura:
http://www.boost.org/community/exception_safety.html
Veja o 7º ponto (Teste automatizado para segurança de exceção), onde ele se baseia em testes de unidade automatizados para garantir que todos os casos sejam testados. Eu acho que essa parte é uma excelente resposta para a pergunta do autor " Você pode ter certeza disso? ".
Edit 31-05-2013: Comentário do dionadar
Dionadar está se referindo à seguinte linha, que de fato tem um comportamento indefinido.
A solução aqui é verificar se o número inteiro já está no seu valor máximo (usando
std::numeric_limits<T>::max()
) antes de fazer a adição.Meu erro iria na seção "Falha normal vs. bug", ou seja, um bug. Ele não invalida o raciocínio e não significa que o código de exceção-seguro seja inútil porque impossível de obter. Você não pode se proteger contra o desligamento do computador, ou erros do compilador, ou mesmo seus erros ou outros erros. Você não pode alcançar a perfeição, mas pode tentar chegar o mais próximo possível.
Corrigi o código com o comentário do Dionadar em mente.
fonte
"finally" does exactly what you were trying to achieve
Claro que sim. E como é fácil produzir código frágilfinally
, a noção de "tentar com o recurso" foi finalmente introduzida no Java 7 (ou seja, 10 anos após os C #using
e 3 décadas após os destruidores de C ++). Essa é a fragilidade que eu critico. Quanto aJust because [finally] doesn't match your taste (RAII doesn't match mine, [...]) doesn't mean it's "failing"
: nisso, a indústria discorda do seu gosto, pois as linguagens Garbage Collected tendem a adicionar instruções inspiradas em RAII (C #using
e Javatry
).RAII doesn't match mine, since it needs a new struct every single darn time, which is tedious sometimes
Não, não. Você pode usar ponteiros inteligentes ou classes de utilitário para "proteger" os recursos.Escrever código com exceção de segurança no C ++ não se resume a usar muitos blocos try {} catch {}. Trata-se de documentar que tipo de garantias fornece seu código.
Eu recomendo a leitura da série Guru da Semana de Herb Sutter , em especial as seções 59, 60 e 61.
Para resumir, existem três níveis de segurança de exceção que você pode fornecer:
Pessoalmente, descobri esses artigos bastante tarde, muito do meu código C ++ definitivamente não é seguro para exceções.
fonte
Alguns de nós usam exceção há mais de 20 anos. A PL / I os possui, por exemplo. A premissa de que são uma tecnologia nova e perigosa me parece questionável.
fonte
Primeiro de tudo (como Neil afirmou), o SEH é o tratamento de exceções estruturadas da Microsoft. É semelhante, mas não idêntico, ao processamento de exceção em C ++. De fato, você precisa habilitar o Manipulação de Exceção C ++, se quiser no Visual Studio - o comportamento padrão não garante que os objetos locais sejam destruídos em todos os casos! Em ambos os casos, o tratamento de exceções não é realmente mais difícil , é apenas diferente .
Agora, para suas perguntas reais.
Sim. Esforço-me pelo código seguro de exceção em todos os casos. Eu evangelizo usando técnicas RAII para escopo de acesso a recursos (por exemplo,
boost::shared_ptr
para memória,boost::lock_guard
para bloqueio). Em geral, o uso consistente de RAII e técnicas de proteção de escopo facilitarão muito a gravação de códigos seguros de exceção. O truque é aprender o que existe e como aplicá-lo.Não. É tão seguro quanto é. Posso dizer que não vi uma falha no processo devido a uma exceção em vários anos de atividade 24/7. Não espero um código perfeito, apenas um código bem escrito. Além de fornecer segurança de exceção, as técnicas acima garantem a correção de uma maneira quase impossível de alcançar com
try
/catch
blocks. Se você estiver capturando tudo em seu escopo de controle superior (encadeamento, processo etc.), pode ter certeza de que continuará executando em face de exceções (na maioria das vezes ). As mesmas técnicas também ajudarão você a continuar a executar corretamente em face de exceções semtry
/catch
bloqueia em todos os lugares .Sim. Você pode ter certeza de uma auditoria de código completa, mas ninguém realmente faz isso? No entanto, revisões regulares de código e desenvolvedores cuidadosos ajudam bastante a chegar lá.
Eu tentei algumas variações ao longo dos anos, como estados de codificação nos bits superiores (ala
HRESULT
s ) ou naquelesetjmp() ... longjmp()
corte horrível . Ambos se decompõem na prática, embora de maneiras completamente diferentes.No final, se você adquirir o hábito de aplicar algumas técnicas e pensar cuidadosamente sobre onde você pode realmente fazer algo em resposta a uma exceção, você terá um código muito legível que é seguro para exceções. Você pode resumir isso seguindo estas regras:
try
/catch
quando pode fazer algo sobre uma exceção específicanew
oudelete
em códigostd::sprintf
,snprintf
e matrizes em geral - usestd::ostringstream
para formatar e substituir matrizes porstd::vector
estd::string
Só posso recomendar que você aprenda a usar as exceções corretamente e esqueça os códigos de resultado se planeja escrever em C ++. Se você deseja evitar exceções, considere escrever em outro idioma que não as possua ou as proteja . Se você realmente quiser aprender a utilizar totalmente o C ++, leia alguns livros de Herb Sutter , Nicolai Josuttis e Scott Meyers .
fonte
new
oudelete
em código": por matéria- Eu acho que você quer dizer fora de um construtor ou destruidor.delete
nunca deve ser usado fora da implementaçãotr1::shared_ptr
e similares.new
pode ser usado desde que seu uso seja algo parecidotr1::shared_ptr<X> ptr(new X(arg, arg));
. A parte importante é que o resultadonew
vai diretamente para um ponteiro gerenciado. A página deboost::shared_ptr
práticas recomendadas descreve as melhores.Não é possível escrever código com exceção de segurança sob o pressuposto de que "qualquer linha pode lançar". O design do código com exceção de segurança depende criticamente de certos contratos / garantias que você deve esperar, observar, seguir e implementar em seu código. É absolutamente necessário ter um código que nunca será lançado. Existem outros tipos de garantias de exceção por aí.
Em outras palavras, criar código com exceção de segurança é, em grande parte, uma questão de design de programa , não apenas uma questão de codificação simples .
fonte
Bem, certamente pretendo.
Tenho certeza de que meus servidores 24/7, construídos usando exceções, funcionam 24 horas por dia, 7 dias por semana e não perdem memória.
É muito difícil ter certeza de que qualquer código está correto. Normalmente, só se pode obter resultados
Não. Usar exceções é mais limpo e fácil do que qualquer uma das alternativas que usei nos últimos 30 anos em programação.
fonte
Deixando de lado a confusão entre as exceções SEH e C ++, você precisa estar ciente de que as exceções podem ser lançadas a qualquer momento e escrever seu código com isso em mente. A necessidade de segurança de exceção é o que impulsiona o uso de RAII, ponteiros inteligentes e outras técnicas modernas de C ++.
Se você seguir os padrões bem estabelecidos, escrever código com exceção de segurança não é particularmente difícil e, na verdade, é mais fácil do que escrever código que lida com retornos de erro corretamente em todos os casos.
fonte
EH é bom, geralmente. Mas a implementação do C ++ não é muito amigável, pois é realmente difícil dizer quão boa é a cobertura de exceção. Java, por exemplo, facilita isso, o compilador tenderá a falhar se você não lidar com possíveis exceções.
fonte
noexcept
.Eu realmente gosto de trabalhar com Eclipse e Java (novo no Java), porque gera erros no editor se você estiver com falta de um manipulador EH. Isso torna muito mais difícil esquecer as coisas para lidar com uma exceção ...
Além disso, com as ferramentas IDE, ele adiciona o bloco try / catch ou outro bloco catch automaticamente.
fonte
Alguns de nós preferem linguagens como Java, que nos obrigam a declarar todas as exceções geradas pelos métodos, em vez de torná-las invisíveis, como em C ++ e C #.
Quando feitas corretamente, as exceções são superiores aos códigos de retorno de erro, se por nenhum outro motivo, você não precisar propagar falhas manualmente na cadeia de chamadas.
Dito isto, a programação da biblioteca de API de baixo nível provavelmente deve evitar o tratamento de exceções e seguir os códigos de retorno de erro.
Tem sido minha experiência que é difícil escrever um código de manipulação de exceção limpo em C ++. Eu acabo usando
new(nothrow)
muito.fonte
new(std::nothrow)
não é suficiente. By the way, é mais fácil de código de exceção-safe escrita em C ++ do que em Java: en.wikipedia.org/wiki/Resource_Acquisition_Is_InitializationEu tento o meu melhor para escrever código com exceção de segurança, sim.
Isso significa que eu tenho o cuidado de observar quais linhas podem ser lançadas. Nem todo mundo pode, e é extremamente importante manter isso em mente. A chave é realmente pensar e projetar seu código para satisfazer as garantias de exceção definidas no padrão.
Esta operação pode ser gravada para fornecer uma forte garantia de exceção? Eu tenho que me contentar com o básico? Quais linhas podem gerar exceções e como posso garantir que, se o fizerem, não corromperão o objeto?
fonte
Você realmente escreve código de exceção seguro? [Nao existe tal coisa. Exceções são um escudo de papel para erros, a menos que você tenha um ambiente gerenciado. Isso se aplica às três primeiras perguntas.]
Você conhece e / ou realmente usa alternativas que funcionam? [Alternativa para o que? O problema aqui é que as pessoas não separam os erros reais da operação normal do programa. Se é uma operação normal do programa (ou seja, um arquivo não encontrado), não é realmente o tratamento de erros. Se for um erro real, não há como manipulá-lo ou não é um erro real. Seu objetivo aqui é descobrir o que deu errado e parar a planilha e registrar um erro, reiniciar o driver na sua torradeira ou apenas rezar para que o caça a jato possa continuar voando mesmo quando o software está com erros e espero o melhor.]
fonte
Muita gente (eu diria mesmo).
O que é realmente importante sobre as exceções é que, se você não escrever nenhum código de manipulação - o resultado será perfeitamente seguro e bem-comportado. Ansioso demais para entrar em pânico, mas seguro.
Você precisa cometer erros ativamente nos manipuladores para obter algo inseguro, e apenas catch (...) {} se compara a ignorar o código de erro.
fonte
f = new foo(); f->doSomething(); delete f;
Se o método doSomething lançar uma exceção, haverá um vazamento de memória.