Você (realmente) escreve código seguro de exceção?

Manipulação de exceção (EH) parece ser o padrão atual, e pesquisando na web, não consigo encontrar novas idéias ou methods que tentam melhorar ou substituí-lo (bem, algumas variações existem, mas nada de novo).

Embora a maioria das pessoas pareça ignorá-lo ou apenas aceitá-lo, o EH tem algumas desvantagens: as exceções são invisíveis para o código e cria muitos, muitos possíveis pontos de saída. Joel no software escreveu um artigo sobre isso . A comparação para goto se encheckbox perfeito, me fez pensar novamente sobre EH.

Eu tento evitar EH e apenas usar valores de retorno, retornos de chamada ou o que for adequado ao propósito. Mas quando você tem que escrever código confiável, você simplesmente não pode ignorar EH nos dias de hoje : ele começa com o new , que pode lançar uma exceção, em vez de apenas retornar 0 (como nos velhos tempos). Isso faz com que qualquer linha de código C ++ seja vulnerável a uma exceção. E então, mais lugares no código fundacional de C ++ lançam exceções … std lib faz isso, e assim por diante.

Isso parece andar em terreno instável .. Então, agora somos obrigados a tomar cuidado com as exceções!

Mas é difícil, é realmente difícil. Você tem que aprender a escrever código seguro de exceção, e mesmo se você tiver alguma experiência com ele, ainda será necessário verificar novamente qualquer linha de código para ser seguro! Ou você começa a colocar blocos try / catch em todos os lugares, o que confunde o código até atingir um estado de ilegível.

EH substituiu a antiga abordagem determinística limpa (valores de retorno ..), que tinha apenas alguns inconvenientes compreensíveis e fáceis de resolver com uma abordagem que cria muitos possíveis pontos de saída em seu código, e se você começar a escrever código que captura exceções (o que você forçados a fazer em algum momento), então ele cria até mesmo uma infinidade de caminhos através de seu código (codifique nos blocos catch, pense em um programa de servidor onde você precisa de outras instalações de log além de std :: cerr ..). EH tem vantagens, mas esse não é o ponto.

Minhas perguntas reais:

  • Você realmente escreve código seguro de exceção?
  • Você tem certeza de que seu último código de “produção pronta” é seguro?
  • Você pode ter certeza disso?
  • Você conhece e / ou realmente usa alternativas que funcionam?

Sua pergunta faz uma afirmação de que “escrever código seguro para exceção é muito difícil”. Primeiro responderei suas perguntas e depois responda à pergunta oculta que está por trás delas.

Respondendo a perguntas

Você realmente escreve código seguro de exceção?

Claro que eu faço.

Esta é a razão pela qual Java perdeu muito do seu apelo para mim como um programador C ++ (falta de semântica RAII), mas estou divagando: Esta é uma questão C ++.

É, de fato, necessário quando você precisa trabalhar com código STL ou Boost. Por exemplo, encadeamentos C ++ ( boost::thread ou std::thread ) lançarão uma exceção para sair normalmente.

Você tem certeza de que seu último código de “produção pronta” é seguro?

Você pode ter certeza disso?

Escrever código seguro para exceção é como escrever código livre de bugs.

Você não pode ter 100% de certeza de que seu código é seguro. Mas, então, você se esforça para isso, usando padrões bem conhecidos e evitando anti-padrões conhecidos.

Você conhece e / ou realmente usa alternativas que funcionam?

Não alternativas viáveis ​​em C ++ (ou seja, você precisará reverter para C e evitar bibliotecas C ++, assim como surpresas externas como o Windows SEH).

Escrevendo código seguro de exceção

Para escrever código seguro de exceção, você deve saber primeiro qual nível de segurança de exceção cada instrução que você escreve é.

Por exemplo, um new pode lançar uma exceção, mas atribuir um built-in (por exemplo, um int ou um ponteiro) não falhará. Uma troca nunca falhará (nunca escreva uma troca lançando), um std::list::push_back pode lançar …

Garantia de exceção

A primeira coisa a entender é que você deve ser capaz de avaliar a garantia de exceção oferecida por todas as suas funções:

  1. none : Seu código nunca deve oferecer isso. Esse código vai vazar tudo e quebrar na primeira exceção lançada.
  2. basic : Esta é a garantia que você deve oferecer, pelo menos, se uma exceção for lançada, nenhum recurso vazar e todos os objects ainda estiverem inteiros
  3. strong : O processamento será bem-sucedido ou lançará uma exceção, mas se for lançado, os dados estarão no mesmo estado como se o processamento não tivesse sido iniciado (isso fornece um poder transacional para o C ++)
  4. nothrow / nofail : O processamento será bem sucedido.

Exemplo de código

O código a seguir parece C ++ correto, mas, na verdade, oferece a garantia “none” e, portanto, não está correto:

 void doSomething(T & t) { if(std::numeric_limits::max() > t.integer) // 1. nothrow/nofail t.integer += 1 ; // 1'. nothrow/nofail X * x = new X() ; // 2. basic : can throw with new and X constructor t.list.push_back(x) ; // 3. strong : can throw x->doSomethingThatCanThrow() ; // 4. basic : can throw } 

Eu escrevo todo o meu código com esse tipo de análise em mente.

A garantia mais baixa oferecida é básica, mas, então, a ordenação de cada instrução faz com que toda a function seja “nenhuma”, porque se 3. lançar, x vazará.

A primeira coisa a fazer seria tornar a function “básica”, ou seja, colocar x em um ponteiro inteligente até que seja de propriedade segura da lista:

 void doSomething(T & t) { if(std::numeric_limits::max() > t.integer) // 1. nothrow/nofail t.integer += 1 ; // 1'. nothrow/nofail std::auto_ptr x(new X()) ; // 2. basic : can throw with new and X constructor X * px = x.get() ; // 2'. nothrow/nofail t.list.push_back(px) ; // 3. strong : can throw x.release() ; // 3'. nothrow/nofail px->doSomethingThatCanThrow() ; // 4. basic : can throw } 

Agora, nosso código oferece uma garantia “básica”. Nada vai vazar, e todos os objects estarão em um estado correto. Mas poderíamos oferecer mais, isto é, a garantia forte. É aí que pode se tornar caro, e é por isso que nem todo código C ++ é forte. Vamos tentar:

 void doSomething(T & t) { // we create "x" std::auto_ptr x(new X()) ; // 1. basic : can throw with new and X constructor X * px = x.get() ; // 2. nothrow/nofail px->doSomethingThatCanThrow() ; // 3. basic : can throw // we copy the original container to avoid changing it T t2(t) ; // 4. strong : can throw with T copy-constructor // we put "x" in the copied container t2.list.push_back(px) ; // 5. strong : can throw x.release() ; // 6. nothrow/nofail if(std::numeric_limits::max() > t2.integer) // 7. nothrow/nofail t2.integer += 1 ; // 7'. nothrow/nofail // we swap both containers t.swap(t2) ; // 8. nothrow/nofail } 

Nós reordenamos as operações, primeiro criando e definindo X para o valor correto. Se alguma operação falhar, então t não é modificado, então, a operação 1 a 3 pode ser considerada “forte”: Se algo for lançado, t não é modificado, e X não vazará porque pertence ao ponteiro inteligente.

Então, criamos uma cópia t2 de t , e trabalhamos nesta cópia da operação 4 para 7. Se algo for lançado, t2 é modificado, mas, então, t ainda é o original. Nós ainda oferecemos a garantia forte.

Então, nós trocamos t e t2 . As operações de troca não devem ser mostradas em C ++, então vamos esperar que a troca que você escreveu para T seja mostrada (se não estiver, reescreva para que não seja mostrada).

Então, se chegarmos ao final da function, tudo terá sucesso (não há necessidade de um tipo de retorno) e t tem seu valor de exceção. Se falhar, t ainda tem seu valor original.

Agora, oferecer a garantia forte pode ser bastante caro, por isso não se esforce para oferecer a garantia forte para todo o seu código, mas se você puder fazê-lo sem um custo (e C ++ inlining e outras otimizações podem fazer todo o código acima) , então faça. O usuário da function agradecerá por isso.

Conclusão

É necessário algum hábito para escrever código seguro para exceção. Você precisará avaliar a garantia oferecida por cada instrução que usar e, em seguida, precisará avaliar a garantia oferecida por uma lista de instruções.

É claro que o compilador C ++ não fará backup da garantia (no meu código, ofereço a garantia como uma tag dowargengen de @warning), o que é meio triste, mas não deve impedi-lo de tentar escrever código seguro para exceção.

Falha normal vs. erro

Como um programador pode garantir que uma function sem falhas será sempre bem-sucedida? Afinal, a function pode ter um bug.

Isso é verdade. As garantias de exceção devem ser oferecidas pelo código livre de bugs. Mas então, em qualquer idioma, chamar uma function supõe que a function está livre de bugs. Nenhum código sano se protege contra a possibilidade de ter um bug. Escreva o melhor código possível e, em seguida, ofereça a garantia com a suposição de que não há erros. E se houver um bug, corrija-o.

As exceções são para falha excepcional no processamento, não para erros de código.

Últimas palavras

Agora, a questão é “Vale a pena?”

Claro que é. Ter uma function “nothrow / no-fail” sabendo que a function não falhará é um grande benefício. O mesmo pode ser dito para uma function “forte”, que permite escrever código com semântica transacional, como bancos de dados, com resources de consolidação / reversão, sendo o commit a execução normal do código, sendo as exceções a reversão.

Então, o “básico” é a garantia mínima que você deve oferecer. C ++ é uma linguagem muito forte lá, com seus escopos, permitindo que você evite qualquer vazamento de resources (algo que um coletor de lixo acharia difícil oferecer para o database, conexão ou identificadores de arquivo).

Então, até onde eu vejo, vale a pena.

Edit 2010-01-29: Sobre o swap não-jogando

nobar fez um comentário que eu acredito, é bastante relevante, porque é parte de “como você escreve código seguro de exceção”:

  • [mim] Uma troca nunca falhará (nem sequer escreva uma troca de arremesso)
  • [nobar] Esta é uma boa recomendação para funções swap() customizadas. Deve-se notar, no entanto, que std::swap() pode falhar com base nas operações que ele usa internamente

o padrão std::swap fará cópias e atribuições, as quais, para alguns objects, podem lançar. Assim, o swap padrão poderia jogar, seja usado para suas classs ou mesmo para classs STL. No que diz respeito ao padrão C ++, a operação de troca para vector , deque e list não será ativada, enquanto que para o map se o functor de comparação puder lançar na construção de uma cópia (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á ativada se os dois vetores tiverem o mesmo alocador (isto é, o caso normal), mas fará cópias se tiverem alocadores diferentes. E assim, eu suponho que poderia jogar neste último caso.

Assim, o texto original ainda é válido: nunca escreva uma troca de lançamento, mas o comentário de nobar deve ser lembrado: certifique-se de que os objects que você está trocando tenham uma troca que não seja de lançamento.

Editar 2011-11-06: artigo interessante

Dave Abrahams , que nos deu as garantias básicas / fortes / não confiáveis , descreveu em um artigo sua experiência em tornar a exceção STL segura:

http://www.boost.org/community/exception_safety.html

Observe o sétimo ponto (teste automatizado de segurança de exceção), onde ele se baseia em testes unitários automatizados para garantir que todos os casos sejam testados. Eu acho que esta parte é uma excelente resposta para o autor da pergunta: ” Você pode ter certeza, que é? “.

Edição 2013-05-31: comentário do dionadar

t.integer += 1; é sem a garantia de que o estouro não irá acontecer, NÃO é uma exceção segura e, na verdade, pode tecnicamente invocar o UB! (Estouro assinado é UB: C ++ 11 5/4 “Se durante a avaliação de uma expressão, o resultado não for matematicamente definido ou não estiver no intervalo de valores representáveis ​​para o seu tipo, o comportamento é indefinido.”) Observe que unsigned unsigned inteiro não transbordam, mas fazem seus cálculos em uma class de equivalência módulo 2 ^ # bits.

Dionadar está se referindo à seguinte linha, que de fato tem um comportamento indefinido.

  t.integer += 1 ; // 1. nothrow/nofail 

A solução aqui é verificar se o inteiro já está em seu valor máximo (usando std::numeric_limits::max() ) antes de fazer a adição.

Meu erro iria na seção “Falha normal vs. erro”, isto é, um bug. Isso não invalida o raciocínio, e isso não significa que o código de exceção segura é inútil porque é impossível de ser alcançado. 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 atingir a perfeição, mas pode tentar chegar o mais perto possível.

Eu corrigi o código com o comentário de Dionadar em mente.

Escrever código seguro para exceção em C ++ não é muito sobre o uso de muitos blocos try {} catch {}. É sobre documentar que tipo de garantias seu código fornece.

Eu recomendo a leitura da série Guru Of The Week de Herb Sutter, em particular as prestações 59, 60 e 61.

Para resumir, existem três níveis de segurança de exceção que você pode fornecer:

  • Básico: quando seu código lança uma exceção, seu código não vaza resources e os objects permanecem destrutíveis.
  • Strong: quando seu código lança uma exceção, ele deixa o estado do aplicativo inalterado.
  • Sem lance: seu código nunca lança exceções.

Pessoalmente, eu descobri esses artigos bem tarde, muito do meu código C ++ definitivamente não é seguro para exceções.

Alguns de nós têm usado exceções há mais de 20 anos. O PL / I tem eles, por exemplo. A premissa de que eles são uma tecnologia nova e perigosa parece questionável para mim.

Primeiro de tudo (como Neil afirmou), o SEH é o Structured Exception Handling da Microsoft. É semelhante, mas não idêntico ao processamento de exceção em C ++. Na verdade, você tem que habilitar o tratamento de exceção do C ++ se você quiser no Visual Studio – o comportamento padrão não garante que os objects locais são destruídos em todos os casos! Em ambos os casos, o tratamento de exceção não é realmente mais difícil , é apenas diferente .

Agora, para suas perguntas reais.

Você realmente escreve código seguro de exceção?

Sim. Eu me esforço para código seguro de exceção em todos os casos. Eu evangelizo usando técnicas RAII para access com escopo a resources (por exemplo, boost::shared_ptr para memory, boost::lock_guard para bloqueio). Em geral, o uso consistente de técnicas RAII e de proteção de escopo tornará o código seguro de exceção muito mais fácil de ser gravado. O truque é aprender o que existe e como aplicá-lo.

Você tem certeza de que seu último código de “produção pronta” é seguro?

Não. É tão seguro quanto é. Eu posso dizer que eu não vi uma falha de processo devido a uma exceção em vários anos de atividade 24/7. Eu 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 exatidão de uma forma quase impossível de ser obtida com blocos try / catch . Se você está pegando tudo em seu escopo de controle superior (thread, processo, etc), então você pode ter certeza que você continuará a executar em caso de exceções (na maioria das vezes ). As mesmas técnicas também ajudarão você a continuar executando corretamente em caso de exceções sem try / catch blocos em todos os lugares .

Você pode mesmo ter certeza de que é?

Sim. Você pode ter certeza de uma auditoria completa do código, mas ninguém realmente faz isso? Revisões regulares de código e desenvolvedores cuidadosos são muito úteis para chegar lá.

Você conhece e / ou realmente usa alternativas que funcionam?

Eu tentei algumas variações ao longo dos anos, como a codificação de estados nos bits superiores (ala HRESULT s ) ou aquele horrível setjmp() ... longjmp() hack. Ambos quebram na prática, embora de maneiras completamente diferentes.


No final, se você adquire 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ê acabará com um código muito legível que é uma exceção segura. Você pode resumir isso seguindo estas regras:

  • Você só quer ver try / catch quando você pode fazer algo sobre uma exceção específica
  • Você quase nunca quer ver uma new bruta ou delete no código
  • Escamw std::sprintf , snprintf , e arrays em geral – use std::ostringstream para formatar e replace arrays com std::vector e std::string
  • Em caso de dúvida, procure funcionalidade no Boost ou no STL antes de fazer sua própria rolagem

Só posso recomendar que você aprenda a usar as exceções corretamente e esqueça os códigos de resultado se você planeja escrever em C ++. Se você quiser evitar exceções, você pode querer considerar escrever em outro idioma que não os tenha ou os torne seguros . Se você quiser realmente aprender como utilizar o C ++, leia alguns livros de Herb Sutter , Nicolai Josuttis e Scott Meyers .

Não é possível escrever código seguro para exceção, sob a suposição de que “qualquer linha pode lançar”. O design do código seguro para exceção se baseia criticamente em certos contratos / garantias que você deve esperar, observar, seguir e implementar em seu código. É absolutamente necessário ter código garantido para nunca jogar. Existem outros tipos de garantias de exceção por aí.

Em outras palavras, a criação de código seguro para exceção é, em grande parte, uma questão de projeto de programa , não apenas uma questão de codificação simples.

  • Você realmente escreve código seguro de exceção?

Bem, eu certamente pretendo.

  • Você tem certeza de que seu último código de “produção pronta” é seguro?

Tenho certeza de que meus servidores 24/7 construídos usando exceções são executados 24 horas por dia e não vazam memory.

  • Você pode ter certeza disso?

É muito difícil ter certeza de que qualquer código esteja correto. Normalmente, só se pode ir pelos resultados

  • Você conhece e / ou realmente usa alternativas que funcionam?

Não. O uso de exceções é mais limpo e fácil do que qualquer uma das alternativas que usei nos últimos 30 anos na programação.

Deixando de lado a confusão entre as exceções do SEH e do C ++, você precisa estar ciente de que 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 é, em grande parte, o que impulsiona o uso de RAII, pointers inteligentes e outras técnicas modernas de C ++.

Se você seguir os padrões bem estabelecidos, escrever código seguro para exceção não é particularmente difícil e, na verdade, é mais fácil do que escrever código que lide com retornos incorretos em todos os casos.

EH é bom, geralmente. Mas a implementação do C ++ não é muito amigável, pois é muito difícil dizer quão boa é sua cobertura de captura de exceção. Java, por exemplo, torna isso fácil, o compilador tenderá a falhar se você não lidar com possíveis exceções.

Eu realmente gosto de trabalhar com Eclipse e Java embora (novo para Java), porque ele gera erros no editor se você estiver faltando um manipulador EH. Isso torna as coisas muito mais difíceis de esquecer para lidar com uma exceção …

Além disso, com as ferramentas IDE, adiciona o bloco try / catch ou outro bloco catch automaticamente.

Alguns de nós preferem linguagens como Java que nos forçam a declarar todas as exceções lançadas por methods, em vez de torná-las invisíveis como em C ++ e C #.

Quando feito corretamente, as exceções são superiores aos códigos de retorno de erro, se por nenhum outro motivo que você não tem que propagar falhas na cadeia de chamada manualmente.

Dito isto, a programação de biblioteca de API de baixo nível provavelmente deve evitar o tratamento de exceções e manter os códigos de retorno de erro.

Tem sido minha experiência que é difícil escrever código de exception handling limpo em C ++. new(nothrow) usando muito new(nothrow) muito.

Eu tento o meu melhor para escrever código seguro, sim.

Isso significa que tomo cuidado para ficar de olho nas linhas que podem ser jogadas. Nem todos podem, 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 escrita para fornecer a garantia de exceção forte? Eu tenho que me contentar com o básico? Quais linhas podem lançar exceções, e como posso garantir que, se o fizerem, não corrompam o object?

  • Você realmente escreve código seguro de exceção? [Nao existe tal coisa. As exceções são uma proteção 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 quê? O problema aqui é que as pessoas não separam 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 um tratamento de erros. Se for um erro real, não há como “manipulá-lo” ou não é um erro real. Seu objective 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 jato possa continuar voando mesmo quando o software estiver cheio de bugs e esperar pelo melhor.]

Muito (eu diria mesmo a maioria) as pessoas fazem.

O que é realmente importante sobre as exceções é que, se você não escrever nenhum código de manipulação, o resultado é perfeitamente seguro e bem-comportado. Muito ansioso para entrar em pânico, mas seguro.

Você precisa cometer erros nos manipuladores ativamente para obter algo inseguro, e somente o catch (…) {} se comparará a ignorar o código de erro.