Devo herdar de std :: exception?

98

Eu vi pelo menos uma fonte confiável (uma classe C ++ que eu fiz) recomendar que as classes de exceção específicas do aplicativo em C ++ devem herdar de std::exception. Não estou certo sobre os benefícios dessa abordagem.

Em C #, as razões para herdar de ApplicationExceptionsão claras: você obtém um punhado de métodos, propriedades e construtores úteis e só precisa adicionar ou substituir o que precisa. Com std::exceptionisso, parece que tudo o que você obtém é um what()método para substituir, que você mesmo poderia criar.

Então, quais são os benefícios, se houver, de usar std::exceptioncomo classe base para minha classe de exceção específica do aplicativo? Existem boas razões para não herdar std::exception?

John M Gant
fonte
Você pode querer dar uma olhada nisso: stackoverflow.com/questions/1605778/1605852#1605852
sbi
2
Embora seja uma observação não relacionada à questão em particular, as classes C ++ que você faz não precisam necessariamente ser fontes confiáveis ​​sobre boas práticas apenas por conta própria.
Christian Rau

Respostas:

69

O principal benefício é que o código que usa suas classes não precisa saber o tipo exato do que você está throwfazendo, mas pode apenas catcho std::exception.

Edit: como Martin e outros observaram, você realmente deseja derivar de uma das subclasses de std::exceptiondeclarado no <stdexcept>cabeçalho.

Nikolai Fetissov
fonte
21
Não há como passar uma mensagem para std :: exception. std :: runtime_error aceita uma string e é derivado de std :: exception.
Martin York
14
Você não deve passar uma mensagem para o construtor de tipo de exceção (considere que as mensagens precisam ser localizadas.) Em vez disso, defina um tipo de exceção que categorize o erro semanticamente, armazene no objeto de exceção o que você precisa para formatar uma mensagem amigável e, em seguida, faça-o no local de captura.
Emil
std::exceptioné definido no <exception>cabeçalho ( prova ). -1
Alexander Shukaev
5
Então, qual é o seu ponto? Definição implica declaração, o que você vê de errado aqui?
Nikolai Fetissov
40

O problema std::exceptioné que não existe um construtor (nas versões compatíveis com o padrão) que aceita uma mensagem.

Como resultado, prefiro derivar de std::runtime_error. Isso é derivado de, std::exceptionmas seus construtores permitem que você passe uma C-String ou um std::stringpara o construtor que será retornado (como a char const*) quando what()for chamado.

Martin York
fonte
11
Não é uma boa ideia formatar uma mensagem amigável no momento do lançamento, porque isso combinaria o código de nível inferior com a funcionalidade de localização e outros enfeites. Em vez disso, armazene no objeto de exceção todas as informações relevantes e deixe o site de captura formatar uma mensagem amigável com base no tipo de exceção e nos dados que ela carrega.
Emil
16
@ Emil: Claro, se você for uma exceção, carregue mensagens que podem ser exibidas pelo usuário, embora geralmente sejam apenas para fins de registro. No site de lançamento, você não tem o contexto para construir uma mensagem para o usuário (já que pode ser uma biblioteca de qualquer maneira). O site de captura terá mais contexto e pode gerar a mensagem apropriada.
Martin York
3
Não sei de onde você tirou a ideia de que as exceções são apenas para fins de registro. :)
Emil,
1
@Emil: Já examinamos essa discussão. Onde eu indiquei que não é isso que você está colocando na exceção. Seu entendimento do assunto parece bom, mas seu argumento é falho. As mensagens de erro geradas para o usuário são completamente diferentes das mensagens de log para o desenvolvedor.
Martin York
1
@Emil. Esta discussão tem um ano de idade. Crie sua própria pergunta neste momento. No que me diz respeito, seus argumentos estão simplesmente errados (e com base em votos as pessoas tendem a concordar comigo). Consulte O que é um 'bom número' de exceções para implementar na minha biblioteca? e Capturar exceções gerais é realmente uma coisa ruim? Você não forneceu nenhuma informação nova desde sua diatribe anterior.
Martin York,
17

A razão para herdar de std::exceptioné uma classe base "padrão" para exceções, portanto, é natural que outras pessoas em uma equipe, por exemplo, esperem isso e capturem a base std::exception.

Se você estiver procurando por conveniência, pode herdar std::runtime_errordaquele std::stringconstrutor de provedores .

Aleksei Potov
fonte
2
Derivar de qualquer outro tipo de exceção padrão, exceto std :: exception, provavelmente não é uma boa idéia. O problema é que eles derivam de std :: exception não virtualmente, o que pode levar a erros sutis envolvendo herança múltipla, onde um catch (std :: exception &) pode falhar silenciosamente em capturar uma exceção devido à conversão para std :: exception sendo ambíguo.
Emil
2
Eu li o artigo sobre impulso no assunto. Parece razoável em um sentido puramente lógico. Mas eu nunca vi MH em exceções na natureza. Nem consideraria usar MH em exceções, então parece uma marreta para consertar um problema inexistente. Se esse fosse um problema real, tenho certeza de que teríamos visto uma ação do comitê de padrões para consertar uma falha tão óbvia. Portanto, minha opinião é que std :: runtime_error (e família) ainda são exceções perfeitamente aceitáveis ​​para lançar ou derivar.
Martin York
5
@Emil: Derivar de outras exceções padrão std::exceptioné uma ideia excelente . O que você não deve fazer é herdar de mais de um.
Mooing Duck
@MooingDuck: Se você deriva de mais de um tipo de exceção padrão, você terminará com std :: exception sendo derivado (não virtualmente) várias vezes. Consulte boost.org/doc/libs/release/libs/exception/doc/… .
Emil,
1
@Emil: Isso decorre do que eu disse.
Mooing Duck
13

Certa vez, participei da limpeza de uma grande base de código onde os autores anteriores haviam lançado ints, HRESULTS, std :: string, char *, classes aleatórias ... coisas diferentes em todos os lugares; basta citar um tipo e provavelmente foi jogado em algum lugar. E nenhuma classe base comum. Acredite em mim, as coisas ficaram muito mais organizadas quando chegamos ao ponto em que todos os tipos lançados tinham uma base comum que poderíamos pegar e saber que nada iria passar. Então, por favor, faça a si mesmo (e àqueles que terão que manter seu código no futuro) um favor e faça assim desde o início.

timday
fonte
12

Você deve herdar de boost :: exception . Ele fornece muito mais recursos e maneiras bem conhecidas de transportar dados adicionais ... claro, se você não estiver usando o Boost , ignore esta sugestão.

James Schek
fonte
5
No entanto, observe que boost :: exception em si não deriva de std :: exception. Mesmo quando derivando de boost :: exception, você também deve derivar de std :: exception (como uma nota separada, sempre que derivar de tipos de exceção, você deve usar herança virtual.)
Emil
10

Sim, você deve derivar std::exception.

Outros responderam que std::exceptiontem o problema de você não conseguir passar uma mensagem de texto para ele; no entanto, geralmente não é uma boa ideia tentar formatar uma mensagem do usuário no momento do lançamento. Em vez disso, use o objeto de exceção para transportar todas as informações relevantes para o site de captura, que pode então formatar uma mensagem amigável.

Emil
fonte
6
+1, bom ponto. Tanto de uma separação de interesses quanto de uma perspectiva i18n, é definitivamente melhor deixar a camada de apresentação construir a mensagem do usuário.
John M Gant
@JohnMGant e Emil: Interessante, você pode apontar um exemplo concreto de como isso pode ser feito. Eu entendo que se pode derivar std::exceptione transportar as informações da exceção. Mas quem será o responsável por construir / formatar a mensagem de erro? A ::what()função ou outra coisa?
alfC
1
Você formatará a mensagem no site de captura, provavelmente no nível do aplicativo (em vez da biblioteca). Você o captura por tipo, analisa as informações e, a seguir, localiza / formata a mensagem de usuário apropriada.
Emil,
9

O motivo pelo qual você pode querer herdar de std::exceptioné porque ele permite que você lance uma exceção que é capturada de acordo com essa classe, ou seja:

class myException : public std::exception { ... };
try {
    ...
    throw myException();
}
catch (std::exception &theException) {
    ...
}
SingleNegationElimination
fonte
6

Há um problema com herança que você deve conhecer é o fatiamento de objetos. Quando você escreve throw e;uma expressão de lançamento, inicializa um objeto temporário, chamado de objeto de exceção, o tipo do qual é determinado removendo quaisquer qualificadores cv de nível superior do tipo estático do operando de throw. Isso pode não ser o que você está esperando. Exemplo de problema que você pode encontrar aqui .

Não é um argumento contra a herança, é apenas uma informação 'obrigatória'.

Kirill V. Lyadvinsky
fonte
3
Acho que o que tirar é "jogar e"; é mau, e "joga"; está bem.
James Schek
2
Sim, throw;está tudo bem, mas não é óbvio que você deva escrever algo assim.
Kirill V. Lyadvinsky
1
É especialmente doloroso para desenvolvedores Java, onde o relançamento é feito usando "throw e;"
James Schek
Considere derivar apenas de tipos de base abstratos. Capturar um tipo base abstrato por valor resultará em um erro (evitando o fatiamento); capturar um tipo de base abstrato por referência e, em seguida, tentar lançar uma cópia dele resultará em um erro (novamente evitando o fatiamento.)
Emil
Você também pode resolver esse problema adicionando uma função membro, raise()como tal: virtual void raise() { throw *this; }. Apenas lembre-se de substituí-lo em cada exceção derivada.
Hora de Justin - Reintegrar Monica
5

Diferença: std :: runtime_error vs std :: exception ()

Se você deve herdar ou não, depende de você. Padrão std::exceptione seus descendentes de padrões propõem uma possível estrutura de hierarquia de exceção (divisão em logic_errorsub - hierarquia e runtime_errorsub - hierarquia) e uma possível interface de objeto de exceção. Se você gosta - use. Se por algum motivo você precisar de algo diferente - defina sua própria estrutura de exceção.

Formiga
fonte
3

Como a linguagem já lança std :: exception, você precisa capturá-la de qualquer maneira para fornecer um relatório de erro decente. Você também pode usar essa mesma captura para todas as suas próprias exceções inesperadas. Além disso, quase qualquer biblioteca que lança exceções as derivaria de std :: exception.

Em outras palavras, é

catch (...) {cout << "Unknown exception"; }

ou

catch (const std::exception &e) { cout << "unexpected exception " << e.what();}

E a segunda opção é definitivamente melhor.


fonte
3

Se todas as suas possíveis exceções derivarem de std::exception, seu bloco catch pode simplesmente catch(std::exception & e)e ter a certeza de capturar tudo.

Depois de capturar a exceção, você pode usar esse whatmétodo para obter mais informações. C ++ não oferece suporte à digitação de pato, portanto, outra classe com um whatmétodo exigiria uma captura diferente e um código diferente para usá-lo.

Mark Ransom
fonte
7
não subclasse std :: exception e então capture (std :: exception e). Você precisa pegar uma referência a std :: exception & (ou um ponteiro) ou então você está cortando os dados da subclasse.
jmucchiello
1
Bem, isso foi um erro estúpido - certamente eu sei disso. stackoverflow.com/questions/1095225/…
Mark Ransom
3

Derivar de qualquer tipo de exceção padrão ou não é a primeira questão. Isso habilita um único manipulador de exceção para todas as exceções da biblioteca padrão e o seu próprio, mas também incentiva esses manipuladores pega-todos. O problema é que só devemos capturar exceções com as quais sabemos lidar. Em main (), por exemplo, capturar todas as exceções std :: é provavelmente uma coisa boa se a string what () for registrada como último recurso antes de sair. Em outro lugar, no entanto, é improvável que seja uma boa ideia.

Depois de decidir se deve derivar de um tipo de exceção padrão ou não, a questão é qual deve ser a base. Se seu aplicativo não precisa de i18n, você pode pensar que formatar uma mensagem no site da chamada é tão bom quanto salvar informações e gerar a mensagem no site da chamada. O problema é que a mensagem formatada pode não ser necessária. Melhor usar um esquema de geração de mensagem preguiçoso - talvez com memória pré-alocada. Então, se a mensagem for necessária, ela será gerada no acesso (e, possivelmente, armazenada em cache no objeto de exceção). Portanto, se a mensagem é gerada quando lançada, então uma derivada std :: exception, como std :: runtime_error é necessária como a classe base. Se a mensagem for gerada lentamente, std :: exception é a base apropriada.

Roubar
fonte
0

Outro motivo para exceções de subclasse é um aspecto de design melhor ao trabalhar em grandes sistemas encapsulados. Você pode reutilizá-lo para coisas como mensagens de validação, consultas do usuário, erros fatais do controlador e assim por diante. Em vez de reescrever ou refazer todas as suas mensagens de validação, você pode simplesmente "pegá-las" no arquivo de origem principal, mas lançar o erro em qualquer lugar em todo o seu conjunto de classes.

por exemplo, uma exceção fatal encerrará o programa, um erro de validação apenas limpará a pilha e uma consulta do usuário fará uma pergunta ao usuário final.

Fazer dessa forma também significa que você pode reutilizar as mesmas classes, mas em interfaces diferentes. por exemplo, um aplicativo do Windows pode usar a caixa de mensagem, um serviço da web mostrará html e o sistema de relatórios irá registrá-lo e assim por diante.

James Burnby
fonte
0

Embora esta pergunta seja bastante antiga e já tenha sido bastante respondida, eu só quero adicionar uma nota sobre como fazer o tratamento adequado de exceções em C ++ 11, uma vez que estou continuamente perdendo isso nas discussões sobre exceções:

Use std::nested_exceptionestd::throw_with_nested

É descrito em StackOverflow aqui e aqui , como você pode obter um backtrace em suas exceções dentro de seu código sem a necessidade de um depurador ou registro complicado, simplesmente escrevendo um manipulador de exceção adequado que irá relançar exceções aninhadas.

Visto que você pode fazer isso com qualquer classe de exceção derivada, você pode adicionar muitas informações a esse backtrace! Você também pode dar uma olhada no meu MWE no GitHub , onde um backtrace seria mais ou menos assim:

Library API: Exception caught in function 'api_function'
Backtrace:
~/Git/mwe-cpp-exception/src/detail/Library.cpp:17 : library_function failed
~/Git/mwe-cpp-exception/src/detail/Library.cpp:13 : could not open file "nonexistent.txt"

Você nem mesmo precisa criar uma subclasse std::runtime_errorpara obter muitas informações quando uma exceção é lançada.

O único benefício que vejo na criação de subclasses (em vez de apenas usar std::runtime_error) é que seu manipulador de exceções pode capturar sua exceção personalizada e fazer algo especial. Por exemplo:

try
{
  // something that may throw
}
catch( const MyException & ex )
{
  // do something specialized with the
  // additional info inside MyException
}
catch( const std::exception & ex )
{
  std::cerr << ex.what() << std::endl;
}
catch( ... )
{
  std::cerr << "unknown exception!" << std::endl;
}
GPMueller
fonte