Devo usar um especificador de exceção em C ++?

123

No C ++, você pode especificar que uma função pode ou não lançar uma exceção usando um especificador de exceção. Por exemplo:

void foo() throw(); // guaranteed not to throw an exception
void bar() throw(int); // may throw an exception of type int
void baz() throw(...); // may throw an exception of some unspecified type

Eu tenho dúvidas sobre realmente usá-los devido ao seguinte:

  1. O compilador não aplica os especificadores de exceção de maneira rigorosa, portanto os benefícios não são grandes. Idealmente, você gostaria de receber um erro de compilação.
  2. Se uma função viola um especificador de exceção, acho que o comportamento padrão é finalizar o programa.
  3. No VS.Net, trata throw (X) como throw (...), portanto, a adesão ao padrão não é forte.

Você acha que especificadores de exceção devem ser usados?
Por favor, responda com "sim" ou "não" e forneça alguns motivos para justificar sua resposta.

1800 INFORMAÇÃO
fonte
7
"throw (...)" não é C ++ padrão. Eu acredito que é uma extensão em alguns compiladores e geralmente tem o mesmo significado que nenhuma especificação de exceção.
Richard Corden

Respostas:

97

Não.

Aqui estão vários exemplos dos motivos:

  1. É impossível escrever o código do modelo com especificações de exceção,

    template<class T>
    void f( T k )
    {
         T x( k );
         x.x();
    }

    As cópias podem ser lançadas, a passagem de parâmetros pode ser lançada e x()pode gerar alguma exceção desconhecida.

  2. As especificações de exceção tendem a proibir a extensibilidade.

    virtual void open() throw( FileNotFound );

    pode evoluir para

    virtual void open() throw( FileNotFound, SocketNotReady, InterprocessObjectNotImplemented, HardwareUnresponsive );

    Você poderia realmente escrever isso como

    throw( ... )

    O primeiro não é extensível, o segundo é ambicioso e o terceiro é realmente o que você quer dizer quando escreve funções virtuais.

  3. Código legado

    Quando você escreve um código que se baseia em outra biblioteca, não sabe realmente o que ele pode fazer quando algo der terrivelmente errado.

    int lib_f();
    
    void g() throw( k_too_small_exception )
    { 
       int k = lib_f();
       if( k < 0 ) throw k_too_small_exception();
    }

    gterminará quando lib_f()jogar. Isso (na maioria dos casos) não é o que você realmente deseja. std::terminate()nunca deve ser chamado. É sempre melhor deixar o aplicativo travar com uma exceção não tratada, da qual você pode recuperar um rastreamento de pilha, do que morrer silenciosa / violentamente.

  4. Escreva um código que retorne erros comuns e jogue em ocasiões excepcionais.

    Error e = open( "bla.txt" );
    if( e == FileNotFound )
        MessageUser( "File bla.txt not found" );
    if( e == AccessDenied )
        MessageUser( "Failed to open bla.txt, because we don't have read rights ..." );
    if( e != Success )
        MessageUser( "Failed due to some other error, error code = " + itoa( e ) );
    
    try
    {
       std::vector<TObj> k( 1000 );
       // ...
    }
    catch( const bad_alloc& b )
    { 
       MessageUser( "out of memory, exiting process" );
       throw;
    }

No entanto, quando sua biblioteca apenas lança suas próprias exceções, você pode usar as especificações de exceção para declarar sua intenção.

Christopher
fonte
1
em 3, seria tecnicamente std :: inesperado, não std :: terminado. Mas quando essa função é chamada, o padrão chama abort (). Isso gera um dump principal. Como isso é pior do que uma exceção não tratada? (que faz basicamente a mesma coisa)
Greg Rogers
6
@ Greg Rogers: Uma exceção não corrigida ainda empilhar o desenrolamento. Isso significa que os destruidores serão chamados. E nesses destruidores, muito pode ser feito, como: Recursos liberados corretamente, logs gravados corretamente, outros processos serão informados que o processo atual está travando, etc. Para resumir, é RAII.
paercebal
Você ficou de fora: Isso efetivamente agrupa tudo em uma try {...} catch (<specified exceptions>) { <do whatever> } catch (...) { unexpected(); ]construção, independentemente de você querer ou não um bloco de tentativa.
David Thornley
4
@ paercebal Isso está incorreto. É definido como implementação se os destruidores são executados ou não para exceções não capturadas. A maioria dos ambientes não desenrola os destruidores de pilha / execução se a exceção não for capturada. Se você deseja garantir que seus destruidores sejam executados de maneira portável, mesmo quando uma exceção é lançada e não tratada (isso é de valor duvidoso), você precisa escrever seu código comotry { <<...code...>> } catch(...) /* stack guaranteed to be unwound here and dtors run */ { throw; /* pass it on to the runtime */ }
Logan Capaldo
" É sempre melhor deixar o aplicativo travar com uma exceção não tratada, da qual você pode recuperar um rastreamento de pilha, do que morrer silenciosa / violentamente. " Como uma "falha" pode ser melhor do que uma chamada limpa terminate()? Por que não basta ligarabort() ?
precisa saber é
42

Evite especificações de exceção em C ++. As razões apresentadas na sua pergunta são um bom começo para o porquê.

Veja "Um olhar pragmático sobre especificações de exceções", de Herb Sutter .

Michael Burr
fonte
3
@awoodland: o uso de 'dynamic-exception-Specifications' ( throw(optional-type-id-list)) está obsoleto no C ++ 11. Eles ainda estão no padrão, mas acho que foi enviada uma advertência de que seu uso deve ser considerado com cuidado. C ++ 11 adiciona a noexceptespecificação e o operador. Não sei detalhes suficientes noexceptpara comentar. Este artigo parece ser bastante detalhado: akrzemi1.wordpress.com/2011/06/10/using-noexcept E Dietmar Kühl tem um artigo no Overload Journal de junho de 2011: accu.org/var/uploads/journals/overload103.pdf
Michael Burr
@MichaelBurr Only throw(something)é considerado inútil e uma má ideia. throw()é útil.
precisa saber é o seguinte
" Aqui está o que muitas pessoas pensam que as especificações de exceção fazem: bullet Garanta que as funções lançem apenas exceções listadas (possivelmente nenhuma). Bullet Habilite otimizações do compilador com base no conhecimento de que somente exceções listadas (possivelmente nenhuma) serão lançadas. As expectativas acima são: novamente, enganosamente perto de estar correto "Não, as expectativas acima estão absolutamente corretas.
curiousguy
14

Eu acho que os
especificadores de exceção convencionalmente, exceto convenção (para C ++), foram um experimento no padrão C ++ que falhou principalmente.
A exceção é que o especificador no throw é útil, mas você também deve adicionar internamente o bloco try try apropriado, para garantir que o código corresponda ao especificador. Herb Sutter tem uma página sobre o assunto. Gotch 82

Além disso, acho que vale a pena descrever as garantias de exceção.

Estas são basicamente a documentação de como o estado de um objeto é afetado por exceções que escapam de um método nesse objeto. Infelizmente, eles não são impostos ou mencionados pelo compilador.
Impulso e exceções

Garantias de exceção

Nenhuma garantia:

Não há garantia sobre o estado do objeto após uma exceção escapar de um método
Nessas situações, o objeto não deve mais ser usado.

Garantia básica:

Em quase todas as situações, essa deve ser a garantia mínima que um método fornece.
Isso garante que o estado do objeto esteja bem definido e ainda possa ser usado de forma consistente.

Garantia Forte: (aka Garantia Transacional)

Isso garante que o método seja concluído com êxito
ou uma exceção será lançada e o estado dos objetos não será alterado.

Nenhuma garantia do lance:

O método garante que nenhuma exceção possa se propagar para fora do método.
Todos os destruidores devem fazer essa garantia.
| NB Se uma exceção escapar de um destruidor enquanto uma exceção já estiver se propagando
| o aplicativo será encerrado

Martin York
fonte
As garantias são algo que todo programador de C ++ deve saber, mas elas não me parecem relacionadas a especificações de exceção.
David Thornley
1
@ David Thornley: Eu vejo as garantias como deveriam ter sido as especificações de exceção (ou seja, um método com G forte não pode chamar um método com um G básico sem proteção). Infelizmente, não tenho certeza de que eles estejam definidos o suficiente para serem aplicados de uma maneira útil, seja o compilador.
Martin York
" Aqui está o que muitas pessoas pensam que as especificações de exceção fazem: - Garanta que as funções lançem apenas exceções listadas (possivelmente nenhuma). - Ative otimizações do compilador com base no conhecimento de que somente as exceções listadas (possivelmente nenhuma) serão lançadas. As expectativas acima são: novamente, enganosamente perto de estar correto. "Na verdade, ambos estão exatamente certos.
precisa saber é o seguinte
@curiousguy. É assim que o Java faz isso porque as verificações são aplicadas em tempo de compilação. C ++ são verificações de tempo de execução. Assim: Guarantee that functions will only throw listed exceptions (possibly none). Não é verdade. Ele garante apenas que, se a função lançar essas exceções, o aplicativo será encerrado. Enable compiler optimizations based on the knowledge that only listed exceptions (possibly none) will be thrownNão pode fazer isso. Como acabei de salientar, você não pode garantir que a exceção não será lançada.
Martin York
1
@LokiAstari "É assim que o Java faz isso porque as verificações são aplicadas no momento da compilação_" A especificação de exceção do Java é um experimento que falhou. Java não possui a especificação de exceção mais útil: throw()(não lança). " Ele garante apenas que, se a função lançar essas exceções, o aplicativo encerrará " Não "não verdadeiro" significa verdadeiro. Não há garantia em C ++ de que uma função nunca chame terminate(). " Como acabei de salientar, você não pode garantir que a exceção não será lançada " Você garante que a função não será lançada . Qual é exatamente o que você precisa.
precisa saber é o seguinte
8

O gcc emitirá avisos quando você violar as especificações de exceção. O que faço é usar macros para usar as especificações de exceção apenas no modo "fiapo", compilado expressamente para verificar se as exceções estão de acordo com a minha documentação.

Jeremy
fonte
7

O único especificador de exceção útil é "throw ()", como em "não lança".

Harold Ekstrom
fonte
2
Você pode, por favor, adicionar um motivo pelo qual é útil?
buti-oxa
2
por que não é útil? não há nada mais útil do que saber que alguma função não começará a lançar exceções deixadas à direita e no centro.
Matt Joiner
1
Por favor, veja a discussão de Herb Sutter mencionada na resposta de Michael Burr para uma explicação detalhada.
Harold Ekstrom
4

As especificações de exceção não são ferramentas maravilhosamente úteis em C ++. No entanto, existe / há um bom uso para eles, se combinado com std :: inesperado.

O que faço em alguns projetos é codificar com especificações de exceção e, em seguida, chamar set_unexpected () com uma função que lançará uma exceção especial do meu próprio design. Essa exceção, na construção, obtém um retorno (de maneira específica da plataforma) e é derivada de std :: bad_exception (para permitir que ela seja propagada, se desejado). Se causar uma chamada terminate (), como geralmente ocorre, o backtrace será impresso com o que () (bem como com a exceção original que a causou; não é difícil encontrá-la) e, assim, recebo informações de onde estava meu contrato violado, como qual exceção inesperada da biblioteca foi lançada.

Se fizer isso, nunca permito a propagação de exceções de biblioteca (exceto as padrão) e derivo todas as minhas exceções de std :: exception. Se uma biblioteca decidir lançar, vou capturar e converter em minha própria hierarquia, permitindo que eu sempre controle o código. Funções modeladas que chamam funções dependentes devem evitar especificações de exceção por razões óbvias; mas é raro ter uma interface de função modelo com código de biblioteca de qualquer maneira (e poucas bibliotecas realmente usam modelos de uma maneira útil).

coppro
fonte
3

Se você estiver escrevendo um código que será usado por pessoas que preferem olhar para a declaração de função do que por comentários, uma especificação informará quais exceções eles podem querer capturar.

Caso contrário, não acho particularmente útil usar nada além throw()de indicar que ele não gera nenhuma exceção.

Branan
fonte
3

Não. Se você usá-los e for lançada uma exceção que você não especificou, seja pelo seu código ou código chamado pelo seu código, o comportamento padrão é encerrar imediatamente o seu programa.

Além disso, acredito que seu uso foi descontinuado nos rascunhos atuais do padrão C ++ 0x.

Ferruccio
fonte
3

Uma especificação "throw ()" permite que o compilador realize algumas otimizações ao fazer uma análise de fluxo de código se souber que a função nunca lançará uma exceção (ou pelo menos promete nunca lançar uma exceção). Larry Osterman fala sobre isso brevemente aqui:

http://blogs.msdn.com/larryosterman/archive/2006/03/22/558390.aspx

O suco
fonte
2

Geralmente eu não usaria especificadores de exceção. No entanto, nos casos em que qualquer outra exceção viesse da função em questão, o programa seria definitivamente incapaz de corrigir , ela poderá ser útil. Em todos os casos, certifique-se de documentar claramente quais exceções poderiam ser esperadas dessa função.

Sim, o comportamento esperado de uma exceção não especificada lançada de uma função com especificadores de exceção é chamar terminate ().

Também observarei que Scott Meyers aborda esse assunto em C ++ mais eficaz. Seu C ++ eficaz e C ++ mais eficaz são livros altamente recomendados.

Kris Kumler
fonte
2

Sim, se você estiver na documentação interna. Ou talvez escrevendo uma biblioteca que outros usem, para que eles possam dizer o que acontece sem consultar a documentação. Jogar ou não jogar pode ser considerado parte da API, quase como o valor de retorno.

Concordo que eles não são realmente úteis para impor a correção do estilo Java no compilador, mas é melhor do que nada ou comentários aleatórios.

user10392
fonte
2

Eles podem ser úteis para testes de unidade, para que, ao escrever os testes, você saiba o que esperar da função quando ela falhar, mas não haverá aplicação ao redor deles no compilador. Eu acho que eles são códigos extras que não são necessários em C ++. Seja qual for a escolha de tudo, você deve ter certeza de que segue o mesmo padrão de codificação no projeto e nos membros da equipe para que seu código permaneça legível.

Ímpar
fonte
0

Do artigo:

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

"É sabido que é impossível escrever um contêiner genérico seguro para exceções." Essa alegação é frequentemente ouvida com referência a um artigo de Tom Cargill [4], no qual ele explora o problema da segurança de exceção para um modelo de pilha genérico. Em seu artigo, Cargill levanta muitas questões úteis, mas infelizmente não apresenta uma solução para seu problema.1 Ele conclui sugerindo que uma solução pode não ser possível. Infelizmente, seu artigo foi lido por muitos como "prova" dessa especulação. Desde que foi publicado, existem muitos exemplos de componentes genéricos seguros para exceções, entre eles os contêineres de biblioteca padrão C ++.

E, de fato, consigo pensar em maneiras de tornar as classes de modelos seguras. A menos que você não tenha controle sobre todas as subclasses, poderá ter um problema de qualquer maneira. Para fazer isso, você pode criar typedefs em suas classes que definem as exceções geradas por várias classes de modelos. Acho que o problema é sempre resolvê-lo depois, em vez de projetá-lo desde o início, e acho que é essa sobrecarga que é o verdadeiro obstáculo.

Marius
fonte
-2

Especificações de exceção = lixo, pergunte a qualquer desenvolvedor Java com mais de 30 anos

Greg Dean
fonte
8
programadores java com mais de 30 anos devem se sentir mal por não conseguirem lidar com C, não têm desculpa para usar java.
Matt Joiner