Jogue a palavra-chave na assinatura da função

199

Qual é a razão técnica pela qual é considerado uma má prática usar a throwpalavra-chave C ++ em uma assinatura de função?

bool some_func() throw(myExc)
{
  ...
  if (problem_occurred) 
  {
    throw myExc("problem occurred");
  }
  ...
}
Konstantin
fonte
Veja esta recente questão relacionada: stackoverflow.com/questions/1037575/…
laalto 28/06/09
1
Isso noexceptmuda alguma coisa?
Aaron McDaid
12
Não há nada errado em ter opiniões sobre algo relacionado à programação. Este critério de fechamento é ruim, pelo menos para esta pergunta. É uma pergunta interessante com respostas interessantes.
Anders Lindén 28/03
Observe que as especificações de exceção estão obsoletas desde o C ++ 11.
François Andrieux 05/04

Respostas:

127

Não, não é considerado uma boa prática. Pelo contrário, é geralmente considerado uma má ideia.

http://www.gotw.ca/publications/mill22.htm entra em muito mais detalhes sobre o porquê, mas o problema é parcialmente que o compilador não consegue impor isso, portanto, ele deve ser verificado em tempo de execução, o que geralmente é indesejável. E não é bem suportado em nenhum caso. (O MSVC ignora as especificações de exceção, exceto throw (), que ele interpreta como garantia de que nenhuma exceção será lançada.

jalf
fonte
25
Sim. Existem maneiras melhores de adicionar espaços em branco ao seu código do que throw (myEx).
Assaf Lavie
4
Sim, as pessoas que descobrem as especificações de exceção geralmente assumem que trabalham como em Java, onde o compilador é capaz de aplicá-las. No C ++, isso não acontece, o que os torna muito menos úteis.
jalf
7
Mas e quanto ao objetivo documentado, então? Além disso, você será informado sobre quais exceções você nunca detectará, mesmo que tente.
Anders Lindén 28/03
1
@ AndersLindén que finalidade documentativa? Se você quiser apenas documentar o comportamento do seu código, basta colocar um comentário acima dele. Não sei o que você quer dizer com a segunda parte. Exceções que você nunca detectará, mesmo que tente?
jalf
4
Eu acho que o código é poderoso quando se trata de documentar seu código. (os comentários podem mentir). O efeito "documentativo" a que me refiro é que você saberá com certeza quais exceções você pode capturar e outras não.
Anders Lindén
57

O Jalf já está vinculado a ele, mas o GOTW explica muito bem por que as especificações de exceção não são tão úteis quanto se poderia esperar:

int Gunc() throw();    // will throw nothing (?)
int Hunc() throw(A,B); // can only throw A or B (?)

Os comentários estão corretos? Nem tanto. Gunc()pode de fato jogar algo e Hunc()pode muito bem jogar algo diferente de A ou B! O compilador apenas garante derrotá-los sem sentido, se o fizerem ... ah, e vencer seu programa sem sentido também, na maioria das vezes.

Isso é exatamente o que acontece, você provavelmente acabará com uma ligação terminate()e seu programa sofrerá uma morte rápida, mas dolorosa.

A conclusão do GOTW é:

Então, aqui está o que parece ser o melhor conselho que nós, como comunidade, aprendemos até hoje:

  • Moral nº 1: nunca escreva uma especificação de exceção.
  • Moral 2: Exceto possivelmente uma vazia, mas se eu fosse você, evitaria isso.
sth
fonte
1
Não sei por que lançaria uma exceção e não seria capaz de mencioná-la. Mesmo que seja acionado por outra função, sei quais exceções podem ser lançadas. A única razão que eu posso ver é porque é tedioso.
MasterMastic
3
@ Ken: O ponto é que escrever especificações de exceção tem conseqüências principalmente negativas. O único efeito positivo é que ele mostra ao programador quais exceções podem ocorrer, mas como não é verificado pelo compilador de uma maneira razoável, é propenso a erros e, portanto, não vale muito.
sth
1
Oh ok, obrigado por responder. Eu acho que é para isso que serve a documentação.
MasterMastic
2
Incorreto. A especificação de exceção deve ser escrita, mas a idéia é comunicar quais erros o chamador deve tentar detectar.
HelloWorld
1
Assim como o @StudentT diz: é responsabilidade da função garantir que não seja lançada mais nenhuma outra exceção. Se o fizerem, o programa termina como deveria. Declarar arremesso significa que não é minha responsabilidade lidar com essa situação e o chamador deve ter informações suficientes para fazer isso. Não declarar exceções significa que elas podem ocorrer em qualquer lugar e podem ser tratadas em qualquer lugar. Isso certamente é uma bagunça anti-OOP. É falha no design capturar exceções em lugares errados. Eu recomendaria não jogar vazio, pois as exceções são excepcionais e a maioria das funções deve jogar vazio de qualquer maneira.
Jan Turon
30

Para adicionar um pouco mais de valor a todas as outras respostas a essa pergunta, deve-se investir alguns minutos na pergunta: Qual é a saída do código a seguir?

#include <iostream>
void throw_exception() throw(const char *)
{
    throw 10;
}
void my_unexpected(){
    std::cout << "well - this was unexpected" << std::endl;
}
int main(int argc, char **argv){
    std::set_unexpected(my_unexpected);
    try{
        throw_exception();
    }catch(int x){
        std::cout << "catch int: " << x << std::endl;
    }catch(...){
        std::cout << "catch ..." << std::endl;
    }
}

Resposta: Conforme observado aqui , o programa chama std::terminate()e, portanto, nenhum dos manipuladores de exceção será chamado.

Detalhes: A primeira my_unexpected()função é chamada, mas como ela não lança novamente um tipo de exceção correspondente para o throw_exception()protótipo da função, no final, std::terminate()é chamada. Portanto, a saída completa fica assim:

user @ user: ~ / tmp $ g ++ -o exceto.teste exceto.test.cpp
usuário @ usuário: ~ / tmp $ ./except.test
bem - isso foi
encerrado inesperadamente chamado depois de lançar uma instância de 'int'
Abortado (núcleo despejado)

John Doe
fonte
12

O único efeito prático do especificador throw é que, se algo diferente myExcfor lançado por sua função, std::unexpectedserá chamado (em vez do mecanismo de exceção não tratado normal).

Para documentar o tipo de exceção que uma função pode lançar, normalmente faço o seguinte:

bool
some_func() /* throw (myExc) */ {
}
Paolo Tedesco
fonte
5
Também é útil observar que uma chamada para std :: inesperado () geralmente resulta em uma chamada para std :: terminate () e o fim abrupto do seu programa.
sth
1
- e que a MSVC pelo menos não implemente esse comportamento até onde eu sei.
jalf
9

Quando as especificações de lançamento foram adicionadas à linguagem, foi com as melhores intenções, mas a prática sustentou uma abordagem mais prática.

Com C ++, minha regra geral é usar apenas especificações de lançamento para indicar que um método não pode ser lançado. Esta é uma garantia forte. Caso contrário, suponha que ele possa jogar qualquer coisa.

Greg D
fonte
9

Bem, enquanto pesquisava sobre essa especificação de lançamento, dei uma olhada neste artigo: - ( http://blogs.msdn.com/b/larryosterman/archive/2006/03/22/558390.aspx )

Também estou reproduzindo uma parte dela aqui, para que possa ser usada no futuro, independentemente do fato do link acima funcionar ou não.

   class MyClass
   {
    size_t CalculateFoo()
    {
        :
        :
    };
    size_t MethodThatCannotThrow() throw()
    {
        return 100;
    };
    void ExampleMethod()
    {
        size_t foo, bar;
        try
        {
            foo = CalculateFoo();
            bar = foo * 100;
            MethodThatCannotThrow();
            printf("bar is %d", bar);
        }
        catch (...)
        {
        }
    }
};

Quando o compilador vê isso, com o atributo "throw ()", ele pode otimizar completamente a variável "bar", porque sabe que não há como lançar uma exceção a partir de MethodThatCannotThrow (). Sem o atributo throw (), o compilador precisa criar a variável "bar", porque se MethodThatCannotThrow lança uma exceção, o manipulador de exceções pode / dependerá do valor da variável de barra.

Além disso, ferramentas de análise de código-fonte como o prefast podem (e usarão) a anotação throw () para melhorar seus recursos de detecção de erros - por exemplo, se você tiver um try / catch e todas as funções que você chamar estiverem marcadas como throw (), você não precisa do try / catch (sim, isso tem um problema se você chamar mais tarde uma função que poderia lançar).

Pratik Singhal
fonte
3

Uma especificação no throw em uma função embutida que retorna apenas uma variável de membro e não pode lançar exceções pode ser usada por alguns compiladores para fazer pessimizações (uma palavra inventada para o oposto de otimizações) que podem ter um efeito prejudicial no desempenho. Isso é descrito na literatura do Boost: especificação de exceção

Com alguns compiladores, uma especificação de não uso em funções não em linha pode ser benéfica se as otimizações corretas forem feitas e o uso dessa função impactar o desempenho da maneira que justifica.

Para mim, parece que usá-lo ou não é uma ligação feita por um olhar muito crítico como parte de um esforço de otimização de desempenho, talvez usando ferramentas de criação de perfil.

Uma citação do link acima para aqueles com pressa (contém um exemplo de efeitos indesejados indesejados da especificação de throw em uma função embutida de um compilador ingênuo):

Justificativa da especificação de exceção

As especificações de exceção [ISO 15.4] às vezes são codificadas para indicar quais exceções podem ser lançadas ou porque o programador espera que elas melhorem o desempenho. Mas considere o seguinte membro de um ponteiro inteligente:

T & operador * () const throw () {return * ptr; }

Esta função não chama outras funções; ele manipula apenas tipos de dados fundamentais como ponteiros. Portanto, nenhum comportamento de tempo de execução da especificação de exceção pode ser invocado. A função é completamente exposta ao compilador; na verdade, é declarado em linha. Portanto, um compilador inteligente pode deduzir facilmente que as funções são incapazes de gerar exceções e fazer as mesmas otimizações que teria feito com base na especificação de exceção vazia. Um compilador "burro", no entanto, pode fazer todos os tipos de pessimizações.

Por exemplo, alguns compiladores desativam o inlining se houver uma especificação de exceção. Alguns compiladores adicionam blocos try / catch. Tais pessimizações podem ser um desastre de desempenho que torna o código inutilizável em aplicativos práticos.

Embora inicialmente atraente, uma especificação de exceção tende a ter conseqüências que requerem uma reflexão muito cuidadosa para serem entendidas. O maior problema com as especificações de exceção é que os programadores as usam como se tivessem o efeito que o programador gostaria, em vez do efeito que realmente têm.

Uma função não embutida é o único local em que uma especificação de exceção "não lança nada" pode ter algum benefício em alguns compiladores.

Pablo Adames
fonte
1
"O maior problema com as especificações de exceção é que os programadores as usam como se tivessem o efeito que o programador gostaria, em vez do efeito que realmente têm". Esta é a principal razão para erros ao se comunicar com máquinas ou pessoas: a diferença entre o que dizemos e o que queremos dizer.
Thagomizer 17/05/19