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
Você realmente escreve código de exceção seguro?
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
ou std::thread
) lançam uma exceção para sair normalmente.
Tem certeza de que seu último código "pronto para produção" é seguro contra exceções?
Você pode ter certeza disso?
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.
Você conhece e / ou realmente usa alternativas que funcionam?
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), um std::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:
- none : seu código nunca deve oferecer isso. Esse código vazará tudo e será quebrado na primeira exceção lançada.
- básico : esta é a garantia que você deve oferecer no mínimo, ou seja, se uma exceção for lançada, nenhum recurso será vazado e todos os objetos ainda serão inteiros
- forte : o processamento será bem-sucedido ou gerará 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 C ++)
- nothrow / nofail : O processamento será bem-sucedido.
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:
void doSomething(T & t)
{
if(std::numeric_limits<int>::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 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:
void doSomething(T & t)
{
if(std::numeric_limits<int>::max() > t.integer) // 1. nothrow/nofail
t.integer += 1 ; // 1'. nothrow/nofail
std::auto_ptr<X> 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 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:
void doSomething(T & t)
{
// we create "x"
std::auto_ptr<X> 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<int>::max() > t2.integer) // 7. nothrow/nofail
t2.integer += 1 ; // 7'. nothrow/nofail
// we swap both containers
t.swap(t2) ; // 8. nothrow/nofail
}
Reordenamos as operações, primeiro criando e configurando X
seu valor correto. Se alguma operação falhar, ela t
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 e X
não vazar porque é de propriedade do ponteiro inteligente.
Então, criamos uma cópia t2
de t
, 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
e t2
. As operações de troca devem ser notowow em C ++, então, esperamos que a troca para a qual você escreveu não T
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":
- [me] Uma troca nunca falha (nem escreva uma troca jogando)
- [nobar] Esta é uma boa recomendação para
swap()
funções personalizadas por escrito . Note-se, no entanto, que std::swap()
pode falhar com base nas operações que utiliza internamente
o 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 para vector
, deque
e list
não será lançada, ao contrário do que poderia acontecer map
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
t.integer += 1;
é sem a garantia de que o excesso não acontecerá NÃO é uma exceção segura e, de fato, pode invocar tecnicamente o UB! (O 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 seu tipo, o comportamento será indefinido.") Observe que não está assinado. inteiro não transborda, mas faz seus cálculos em uma classe 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 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.