Como posso criar e aplicar contratos para exceções?

33

Estou tentando convencer o líder da minha equipe a permitir o uso de exceções em C ++ em vez de retornar um bool isSuccessfulou uma enumeração com o código de erro. No entanto, não posso contrariar essa crítica dele.

Considere esta biblioteca:

class OpenFileException() : public std::runtime_error {
}

void B();
void C();

/** Does blah and blah. */
void B() {
    // The developer of B() either forgot to handle C()'s exception or
    // chooses not to handle it and let it go up the stack.
    C();
};

/** Does blah blah.
 *
 * @raise OpenFileException When we failed to open the file. */
void C() {
    throw new OpenFileException();
};
  1. Considere um desenvolvedor chamando a B()função Ele verifica sua documentação e vê que ela não retorna exceções, para não tentar pegar nada. Esse código pode travar o programa em produção.

  2. Considere um desenvolvedor chamando a C()função Ele não verifica a documentação, portanto não captura nenhuma exceção. A chamada é insegura e pode travar o programa em produção.

Mas se verificarmos erros desta maneira:

void old_C(myenum &return_code);

Um desenvolvedor que use essa função será avisado pelo compilador se ele não fornecer esse argumento e ele diria "Aha, isso retorna um código de erro que devo verificar".

Como posso usar exceções com segurança, para que haja algum tipo de contrato?

DBedrenko
fonte
4
@ Sjoerd A principal vantagem é que um desenvolvedor é forçado pelo compilador a fornecer uma variável para a função para armazenar o código de retorno; ele é informado de que pode haver um erro e ele deve lidar com isso. Isso é infinitamente melhor que as exceções, que não têm verificação de tempo de compilação.
DBedrenko
4
"ele deve lidar com isso" - não há como verificar se isso também acontece.
Sjoerd
4
@ Sjoerd Não é necessário. É bom o suficiente para informar ao desenvolvedor que um erro pode ocorrer e que ele deve ser verificado. Também está à vista dos revisores, se eles verificaram um erro ou não.
DBedrenko
5
Você pode ter uma melhor chance usando um Either/ Resultmônada para retornar o erro de uma forma combináveis tipo seguro
Daenyth
5
@ Gardenhead Como muitos desenvolvedores não o usam corretamente, acabam com códigos feios e passam a culpar o recurso em vez de si mesmos. O recurso de exceção verificada em Java é uma coisa bonita, mas fica ruim porque algumas pessoas simplesmente não entendem.
AxiomaticNexus

Respostas:

47

Esta é uma crítica legítima às exceções. Eles geralmente são menos visíveis do que o tratamento simples de erros, como retornar um código. E não há uma maneira fácil de fazer cumprir um "contrato". Parte do objetivo é permitir que você permita que as exceções sejam capturadas em um nível mais alto (se você precisar capturar todas as exceções em todos os níveis, qual a diferença de retornar um código de erro?). E isso significa que seu código pode ser chamado por algum outro código que não lida adequadamente com ele.

Exceções têm desvantagens; você precisa argumentar com base no custo-benefício.
Achei esses dois artigos úteis: A necessidade de exceções e Tudo errado com exceções. Além disso, esta postagem do blog oferece opiniões de muitos especialistas sobre exceções, com foco em C ++ . Embora a opinião de especialistas pareça inclinar-se a favor de exceções, está longe de ser um consenso claro.

Quanto a convencer o líder da sua equipe, talvez essa não seja a batalha certa a escolher. Especialmente não com código legado. Conforme observado no segundo link acima:

As exceções não podem ser propagadas por qualquer código que não seja seguro para exceções. O uso de exceções implica, portanto, que todo o código no projeto deve ser protegido contra exceções.

Adicionar um pouco de código que usa exceções a um projeto que principalmente não é provavelmente não será uma melhoria. Não usar exceções em códigos bem escritos está longe de ser um problema catastrófico; pode não ser um problema, dependendo do aplicativo e de qual especialista você pergunta. Você tem que escolher suas batalhas.

Provavelmente, esse não é um argumento no qual eu gastaria esforços - pelo menos até que um novo projeto seja iniciado. E mesmo se você tiver um novo projeto, ele será usado ou usado por algum código legado?


fonte
1
Obrigado pelo conselho e os links. Este é um projeto novo, não legado. Estou me perguntando por que as exceções são tão comuns quando elas têm uma desvantagem tão grande que torna seu uso inseguro. Se você capturar uma exceção n-níveis mais altos e, posteriormente, modificar o código e movê-lo, não haverá verificação estática para garantir que a exceção ainda seja capturada e tratada (e é pedir demais aos revisores que verifiquem todas as funções n-níveis) )
DBedrenko
3
@ Veja os links acima para alguns argumentos a favor de exceções (adicionei um link adicional com mais opinião de especialistas).
6
Esta resposta está no local!
Doc Brown
1
Posso afirmar por experiência que tentar adicionar exceções a uma base de código preexistente é uma idéia MUITO ruim. Eu trabalhei com essa base de código e foi MUITO, MUITO difícil trabalhar porque você nunca sabia o que ia explodir na sua cara. Exceções somente devem ser usadas em projetos nos quais você planeja antecipadamente.
31916 Michael Kohne
26

Existem literalmente livros inteiros escritos sobre esse assunto, então qualquer resposta será, na melhor das hipóteses, um resumo. Aqui estão alguns dos pontos importantes que acho que valem a pena destacar, com base na sua pergunta. Não é uma lista exaustiva.


Exceções não devem ser capturadas em todo o lugar.

Contanto que exista um manipulador de exceção geral no loop principal - dependendo do tipo de aplicativo (servidor web, serviço local, utilitário de linha de comando ...) - você geralmente possui todos os manipuladores de exceção necessários.

No meu código, existem apenas algumas instruções catch - se houver - fora do loop principal. E essa parece ser a abordagem comum no C ++ moderno.


Exceções e códigos de retorno não são mutuamente exclusivos.

Você não deve fazer disso uma abordagem do tipo tudo ou nada. Exceções devem ser usadas para situações excepcionais. Coisas como "Arquivo de configuração não encontrado", "Disco cheio" ou qualquer outra coisa que não possa ser manipulada localmente.

Falhas comuns, como verificar se um nome de arquivo fornecido pelo usuário é válido, não são um caso de uso para exceções; Use um valor de retorno nesses casos.

Como você vê nos exemplos acima, "arquivo não encontrado" pode ser uma exceção ou um código de retorno, dependendo do caso de uso: "faz parte da instalação" versus "o usuário pode fazer um erro de digitação".

Portanto, não há regra absoluta. Uma diretriz aproximada é: se puder ser tratada localmente, faça com que seja um valor de retorno; se você não puder lidar com isso localmente, lance uma exceção.


A verificação estática de exceções não é útil.

Como as exceções não devem ser tratadas localmente de qualquer maneira, geralmente não é importante quais exceções podem ser lançadas. A única informação útil é se alguma exceção pode ser lançada.

O Java possui verificação estática, mas geralmente é considerado um experimento com falha e a maioria das linguagens, já que - principalmente o C # - não tem esse tipo de verificação estática. Esta é uma boa leitura sobre os motivos pelos quais o C # não o possui.

Por esses motivos, o C ++ substituiu o seu throw(exceptionA, exceptionB)em favor de noexcept(true). O padrão é que uma função pode ser lançada, então os programadores devem esperar isso, a menos que a documentação prometa explicitamente o contrário.


Escrever código de exceção seguro não tem nada a ver com escrever manipuladores de exceção.

Prefiro dizer que escrever código de exceção seguro é tudo sobre como evitar escrever manipuladores de exceção!

A maioria das práticas recomendadas pretende reduzir o número de manipuladores de exceção. Escrever um código uma vez e invocá-lo automaticamente - por exemplo, através do RAII - resulta em menos erros do que copiar e colar o mesmo código em todo o lugar.

Sjoerd
fonte
3
Desde que exista um manipulador de exceção geral ... você geralmente já está bem. ” Isso contradiz a maioria das fontes, o que enfatiza que é muito desafiador escrever código de exceção seguro.
12
@ dan1111 "Escrever código de exceção seguro" tem pouco a ver com escrever manipuladores de exceção. Escrever código de exceção seguro é sobre o uso da RAII para encapsular recursos, o uso de "todo o trabalho local primeiro e depois a troca / movimentação" e outras práticas recomendadas. Esses são desafiadores quando você não está acostumado a eles. Mas "escrever manipuladores de exceção" não faz parte dessas práticas recomendadas.
Sjoerd
4
@AxiomaticNexus Em algum nível, você pode explicar todos os usos malsucedidos de um recurso como "maus desenvolvedores". As melhores idéias são as que reduzem o mau uso, porque simplificam a execução da coisa certa. As exceções verificadas estaticamente não se enquadram nessa categoria. Eles criam um trabalho desproporcional ao seu valor, geralmente em locais onde o código que está sendo escrito simplesmente nem precisa se preocupar com eles. Os usuários são tentados a silenciá-los de maneiras erradas para fazer o compilador parar de incomodá-los. Mesmo bem, eles causam muita confusão.
jpmc26
4
@AxiomaticNexus A razão pela qual é desproporcional é que isso pode se propagar facilmente para quase todas as funções em toda a sua base de códigos . Quando todas as funções na metade das minhas classes acessam o banco de dados, nem todas precisam declarar explicitamente que pode gerar uma SQLException; nem todo método controlador que os chama. Tanto barulho é, na verdade, um valor negativo, independentemente de quão pequena seja a quantidade de trabalho. O Sjoerd bate na cabeça: a maior parte do seu código não precisa se preocupar com exceções, porque de qualquer maneira não pode fazer nada a respeito .
jpmc26
3
@AxiomaticNexus Sim, porque os melhores desenvolvedores são tão perfeitos que seu código sempre lidará perfeitamente com todas as entradas possíveis do usuário, e você nunca verá essas exceções no prod. E se o fizer, significa que você é um fracasso na vida. Sério, não me dê esse lixo. Ninguém é tão perfeito, nem mesmo o melhor. E seu aplicativo deve se comportar de maneira sadia, mesmo diante desses erros, mesmo que "sã" seja "pare e retorne uma página / mensagem de erro meio decente". E adivinhe: é a mesma coisa que você faz com 99% de IOExceptions também . A divisão marcada é arbitrária e inútil.
jpmc26
8

Os programadores de C ++ não procuram especificações de exceção. Eles procuram garantias de exceção.

Suponha que um pedaço de código tenha lançado uma exceção. Que suposições o programador pode fazer que ainda serão válidas? Da maneira como o código é escrito, o que o código garante após uma exceção?

Ou é possível que um determinado pedaço de código possa garantir que nunca será lançado (ou seja, nada menos que o processo do SO ser encerrado)?

A palavra "retroceder" ocorre frequentemente em discussões sobre exceções. Ser capaz de reverter para um estado válido (explicitamente documentado) é um exemplo de garantia de exceção. Se não houver garantia de exceção, um programa deve ser encerrado no local porque nem sequer é garantido que qualquer código executado posteriormente funcione conforme o esperado - por exemplo, se a memória estiver corrompida, qualquer operação adicional será um comportamento tecnicamente indefinido.

Várias técnicas de programação C ++ promovem garantias de exceção. O RAII (gerenciamento de recursos baseado em escopo) fornece um mecanismo para executar o código de limpeza e garantir que os recursos sejam liberados em casos normais e casos excepcionais. Fazer uma cópia dos dados antes de realizar modificações nos objetos permite restaurar o estado desse objeto se a operação falhar. E assim por diante.

As respostas para esta pergunta do StackOverflow dão uma idéia dos grandes comprimentos que os programadores de C ++ compreendem todos os possíveis modos de falha que podem acontecer ao seu código e tentam proteger a validade do estado do programa, apesar das falhas. A análise linha a linha do código C ++ se torna um hábito.

Ao desenvolver em C ++ (para uso em produção), não se pode dar ao luxo de encobrir os detalhes. Além disso, o blob binário (de código não aberto) é a desgraça dos programadores de C ++. Se eu precisar chamar algum blob binário e o blob falhar, a engenharia reversa é o que um programador C ++ faria a seguir.

Referência: http://en.cppreference.com/w/cpp/language/exceptions#Exception_safety - consulte em Segurança de exceção.

O C ++ teve uma falha na tentativa de implementar especificações de exceção. Análises posteriores em outros idiomas afirmam que as especificações de exceção simplesmente não são práticas.

Por que é uma tentativa fracassada: para aplicá-lo estritamente, ele deve fazer parte do sistema de tipos. Mas não é. O compilador não verifica a especificação de exceção.

Por que o C ++ escolheu isso e por que as experiências de outras linguagens (Java) provam que a especificação de exceção é discutível: À medida que se modifica a implementação de uma função (por exemplo, ele precisa fazer uma chamada para uma função diferente que pode gerar um novo tipo de exceção), uma aplicação estrita de especificação de exceção significa que você também deve atualizar essa especificação. Isso se propaga - você pode ter que atualizar as especificações de exceção para dezenas ou centenas de funções para uma simples alteração. As coisas pioram para as classes base abstratas (o equivalente em C ++ das interfaces). Se a especificação de exceção for imposta nas interfaces, as implementações de interfaces não poderão chamar funções que geram tipos diferentes de exceções.

Referência: http://www.gotw.ca/publications/mill22.htm

Começando com C ++ 17, o [[nodiscard]]atributo pode ser usado em valores de retorno de função (consulte: https://stackoverflow.com/questions/39327028/can-ac-function-be-declared-such-that-the-return-value não pode ser ignorado ).

Então, se eu fizer uma alteração no código e isso introduzir um novo tipo de condição de falha (ou seja, um novo tipo de exceção), será uma mudança de quebra? Deveria ter obrigado o chamador a atualizar o código ou pelo menos ser avisado sobre a alteração?

Se você aceitar os argumentos de que os programadores de C ++ procuram garantias de exceção em vez de especificações de exceção, a resposta é que, se o novo tipo de condição de falha não quebrar nenhuma das exceções, garantir que o código promete anteriormente, não será uma mudança de quebra.

rwong
fonte
"Quando alguém modifica a implementação de uma função (por exemplo, ele precisa fazer uma chamada para uma função diferente que pode gerar um novo tipo de exceção), uma aplicação rigorosa de especificação de exceção significa que você também precisa atualizar essa especificação" - Bom , como você deveria. Qual seria a alternativa? Ignorando a exceção recém-introduzida?
AxiomaticNexus 28/10
@AxiomaticNexus Isso também é respondido por garantia de exceção. Na verdade, argumento que a especificação nunca pode ser completa; garantia é o que procuramos. Frequentemente, comparamos o tipo de exceção na tentativa de descobrir qual garantia foi quebrada - para adivinhar qual garantia ainda era válida. A especificação de exceção lista alguns dos tipos que você precisa verificar, mas lembre-se de que em qualquer sistema prático, a lista não pode estar completa. Na maioria das vezes, força as pessoas a pegar o atalho de "comer" o tipo de exceção, envolvendo-o em outra exceção, o que dificulta a verificação do tipo de exceção
rwong 28/16/16
@AxiomaticNexus Não estou a favor ou contra o uso de exceções. O uso típico de exceção incentiva a existência de uma classe de exceção básica e de algumas classes de exceção derivadas. Enquanto isso, é preciso lembrar que outros tipos de exceção são possíveis, por exemplo, aqueles lançados do C ++ STL ou de outras bibliotecas. Pode-se argumentar que a especificação de exceção pode ser feita para funcionar neste caso: especifique que as exceções padrão ( std::exception) e sua própria exceção base serão lançadas. Mas quando aplicada a um projeto inteiro, significa simplesmente que cada função terá a mesma especificação: ruído.
rwong
Eu apontaria que quando se trata de exceções, C ++ e Java / C # são linguagens diferentes. Em primeiro lugar, o C ++ não é seguro para tipos, no sentido de permitir a execução arbitrária de códigos ou a substituição de dados, em segundo lugar, o C ++ não imprime um bom rastreamento de pilha. Essas diferenças forçaram os programadores de C ++ a fazer escolhas diferentes em termos de tratamento de erros e exceções. Isso também significa que determinados projetos podem legitimamente alegar que o C ++ não é uma linguagem adequada para eles. (Por exemplo, eu, pessoalmente, considero que TIFF decodificação imagem não será permitida a ser implementado em C ou C ++.)
rwong
1
@rwong: Um grande problema com o tratamento de exceções em linguagens que seguem o modelo C ++ é que catchse baseia no tipo de objeto de exceção, quando em muitos casos o que importa não é tanto a causa direta da exceção, mas o que ela implica sobre o estado do sistema. Infelizmente, o tipo de exceção geralmente não diz nada sobre se causou a interrupção do código de maneira disruptiva de um trecho de código de uma maneira que deixou um objeto em um estado parcialmente atualizado (e, portanto, inválido).
Supercat 28/10
4

Considere um desenvolvedor chamando a função C (). Ele não verifica a documentação, portanto não captura nenhuma exceção. A chamada não é segura

É totalmente seguro. Ele não precisa capturar exceções em todos os lugares, ele pode apenas tentar / capturar em um lugar onde ele possa realmente fazer algo útil sobre isso. Não é seguro se ele permitir que ele vaze de um thread, mas normalmente não é difícil impedir que isso aconteça.

DeadMG
fonte
É inseguro, pois ele não espera uma exceção C()e, consequentemente, não protege contra o código após a chamada ser ignorada. Se esse código for necessário para garantir que alguma invariância permaneça verdadeira, essa garantia será prejudicada na primeira exceção que surgir C(). Não existe software perfeitamente seguro para exceções e, certamente, não onde as exceções nunca foram esperadas.
#
1
@ master O fato de ele não esperar uma exceçãoC() é um grande erro. O padrão em C ++ é que qualquer função pode ser lançada - requer um explícito noexcept(true)para informar o compilador de outra forma. Na ausência desta promessa, um programador deve assumir que uma função pode ser lançada.
Sjoerd
1
@ master Essa é sua própria culpa idiota e nada a ver com o autor de C (). Exceção à segurança é uma coisa, ele deve aprender e segui-la. Se ele chama uma função que ele não sabe que não é exceção, ele deve muito bem lidar com o que acontece se ele for lançado. Se ele não precisar de exceção, ele deve encontrar uma implementação que seja.
DeadMG
@Sjoerd e DeadMG: Embora o padrão permita que qualquer função lance uma exceção, isso não significa que os projetos devem permitir exceções em seu código. Se você banir exceções do seu código, assim como o líder da equipe do OP parece estar fazendo, você não precisa fazer uma programação segura para exceções. E se você tentar convencer um líder de equipe a permitir exceções em uma base de código que anteriormente era livre de exceções, haverá várias C()funções quebradas na presença de exceções. Isso é realmente uma coisa muito imprudente, está fadado a levar a uma tonelada de problemas.
cmaster
@cmaster Trata-se de um novo projeto - veja o primeiro comentário sobre a resposta aceita. Portanto, não existem funções existentes que serão interrompidas, pois não existem funções existentes.
Sjoerd
3

Se você estiver construindo um sistema crítico, considere seguir o conselho do líder da equipe e não use exceções. Esta é a regra 208 da AV nos padrões de codificação C ++ do veículo aéreo de combate conjunto da Lockheed Martin . Por outro lado, as diretrizes MISRA C ++ possuem regras muito específicas sobre quando exceções podem e não podem ser usadas se você estiver criando um sistema de software compatível com MISRA.

Se você estiver construindo sistemas críticos, é provável que também esteja executando ferramentas de análise estática. Muitas ferramentas de análise estática alertam se você não verificar o valor de retorno de um método, tornando casos de manipulação de erros ausentes prontamente aparentes. Que eu saiba, suporte de ferramenta semelhante para detectar o tratamento adequado de exceções não é tão forte.

Por fim, eu argumentaria que o design por contrato e programação defensiva, juntamente com a análise estática, é mais seguro para sistemas críticos de software do que exceções.

Thomas Owens
fonte