O uso de assert () é uma má prática em C ++?

93

Eu tendo a adicionar muitas asserções ao meu código C ++ para tornar a depuração mais fácil sem afetar o desempenho das compilações. Agora,assert é uma macro C pura projetada sem mecanismos C ++ em mente.

Por outro lado std::logic_error, C ++ define , que deve ser lançado nos casos em que há um erro na lógica do programa (daí o nome). Lançar uma instância pode ser a alternativa perfeita e mais C ++ para assert.

O problema é que asserte abortambos terminam o programa imediatamente sem chamar destruidores, portanto, ignorando a limpeza, enquanto lançar uma exceção manualmente adiciona custos de tempo de execução desnecessários. Uma maneira de contornar isso seria criar uma macro de asserção própria SAFE_ASSERT, que funciona exatamente como a contraparte C, mas lança uma exceção em caso de falha.

Posso pensar em três opiniões sobre este problema:

  • Atenha-se à afirmação de C. Como o programa é encerrado imediatamente, não importa se as alterações foram desenroladas corretamente. Além disso, usar #defines em C ++ é tão ruim quanto.
  • Lance uma exceção e capture-a em main () . Permitir que o código ignore destruidores em qualquer estado do programa é uma prática ruim e deve ser evitado a todo custo, assim como as chamadas para terminate (). Se exceções são lançadas, elas devem ser capturadas.
  • Lance uma exceção e deixe-o encerrar o programa. Uma exceção ao encerrar um programa está bem e, devido a NDEBUG, isso nunca acontecerá em uma versão de construção. A captura é desnecessária e expõe detalhes de implementação do código interno a main().

Existe uma resposta definitiva para este problema? Alguma referência profissional?

Editado: Ignorar destruidores, obviamente, não é um comportamento indefinido.

Fabian Knorr
fonte
22
Não, realmente, logic_erroré o erro lógico. Um erro na lógica do programa é chamado de bug. Você não resolve bugs lançando exceções.
R. Martinho Fernandes
4
Asserções, exceções, códigos de erro. Cada um tem um caso de uso completamente distinto e você não deve usar um onde o outro é necessário.
Kerrek SB de
5
Certifique-se de usar static_assertonde for apropriado, se tiver disponível.
Flexo
4
@trion Não vejo como isso ajuda. Você jogaria std::bug?
R. Martinho Fernandes
3
@trion: Não faça isso. As exceções não são para depuração. Alguém pode estar pegando a exceção. Não há necessidade de se preocupar com o UB ao ligar std::abort(); ele apenas levantará um sinal que fará com que o processo seja encerrado.
Kerrek SB de

Respostas:

73

As asserções são inteiramente apropriadas no código C ++. As exceções e outros mecanismos de tratamento de erros não têm o mesmo objetivo que as asserções.

O tratamento de erros é para quando há um potencial para recuperar ou relatar um erro de forma adequada ao usuário. Por exemplo, se houver um erro ao tentar ler um arquivo de entrada, você pode querer fazer algo a respeito. Erros podem resultar de bugs, mas também podem ser simplesmente a saída apropriada para uma determinada entrada.

Asserções são para coisas como verificar se os requisitos de uma API são atendidos quando a API normalmente não seria verificada, ou para verificar coisas que o desenvolvedor acredita estar garantido pela construção. Por exemplo, se um algoritmo requer entrada classificada, você normalmente não verificaria isso, mas você pode ter uma declaração para verificá-lo para que as compilações de depuração sinalizem esse tipo de bug. Uma afirmação deve sempre indicar um programa operando incorretamente.


Se você está escrevendo um programa em que um desligamento incorreto pode causar um problema, convém evitar afirmações. O comportamento indefinido estritamente em termos da linguagem C ++ não se qualifica como um problema aqui, uma vez que acertar uma asserção provavelmente já é o resultado de um comportamento indefinido ou a violação de algum outro requisito que poderia impedir que alguma limpeza funcionasse corretamente.

Além disso, se você implementar asserções em termos de uma exceção, ela poderá ser potencialmente capturada e 'tratada', embora isso contradiga o próprio propósito da asserção.

bames53
fonte
1
Não tenho certeza se isso foi declarado especificamente na resposta, então vou declarar aqui: você não deve usar uma asserção para nada que envolva entrada do usuário que não possa ser determinada no momento da escrita do código. Se um usuário passa ao 3invés do 1seu código, em geral, isso não deve acionar uma asserção. As asserções são apenas erros do programador, não do usuário da biblioteca ou erro do aplicativo.
SS Anne
101
  • As afirmações são para depuração . O usuário do código enviado nunca deve vê-los. Se uma asserção for atingida, seu código precisará ser corrigido.

  • As exceções são para circunstâncias excepcionais . Se um for encontrado, o usuário não poderá fazer o que deseja, mas poderá continuar em outro lugar.

  • O tratamento de erros é para o fluxo normal do programa. Por exemplo, se você solicitar ao usuário um número e obter algo que não pode ser analisado, isso é normal , porque a entrada do usuário não está sob seu controle e você sempre deve lidar com todas as situações possíveis como algo natural. (Por exemplo, faça um loop até obter uma entrada válida, dizendo "Desculpe, tente novamente" no meio.)

Kerrek SB
fonte
1
veio procurando por este re afirmar; qualquer forma de declaração que passa pelo código de produção aponta para um design e controle de qualidade deficientes. O ponto onde um assert é chamado é onde deve estar o manuseio elegante de uma condição de erro. (Eu nunca uso assert's). Quanto às exceções, o único caso de uso que conheço é quando o ctor pode falhar, todos os outros são para tratamento de erros normal.
slashmais
5
@slashmais: O sentimento é louvável, mas a menos que você esteja enviando um código perfeito e sem bugs, acho uma afirmação (mesmo que travar o usuário) preferível a um comportamento indefinido. Bugs acontecem em sistemas complexos, e com uma afirmação você tem uma maneira de ver e diagnosticar onde eles acontecem.
Kerrek SB
@KerrekSB Eu prefiro usar uma exceção em vez de uma afirmação. Pelo menos o código tem uma chance de descartar o branch com falha e fazer outra coisa útil. No mínimo, se você estiver usando RAII, todos os buffers para abrir arquivos serão liberados corretamente.
daemonspring
14

As asserções podem ser usadas para verificar invariantes de implementação internos, como estado interno antes ou depois da execução de algum método, etc. Se a asserção falhar, isso realmente significa que a lógica do programa está quebrada e você não pode se recuperar disso. Nesse caso, o melhor que você pode fazer é interromper o mais rápido possível, sem passar exceção para o usuário. O que é realmente bom sobre asserções (pelo menos no Linux) é que o dump de núcleo é gerado como resultado do encerramento do processo e, portanto, você pode investigar facilmente o rastreamento de pilha e as variáveis. Isso é muito mais útil para entender a falha lógica do que a mensagem de exceção.

nogard
fonte
Eu tenho uma abordagem semelhante. Eu uso asserções para lógica que provavelmente devem ser corretas localmente (por exemplo, invariantes de loop). As exceções são para quando um erro lógico foi forçado no código por uma situação não local (externa).
spraff
Se uma afirmação falhar, significa que a lógica de parte do programa está quebrada. Uma afirmação falhada não significa necessariamente que nada pode ser realizado. Um plugin quebrado provavelmente não deveria abortar um processador de texto inteiro.
daemonspring
13

Não rodar destruidores devido a alling abort () não é um comportamento indefinido!

Se fosse, seria um comportamento indefinido chamar std::terminate()também e, portanto, de que adiantaria fornecê-lo?

assert() é tão útil em C ++ quanto em C. As afirmações não são para tratamento de erros, mas para abortar o programa imediatamente.

Jonathan Wakely
fonte
1
Eu diria que abort()é para abortar o programa imediatamente. Você está certo que as asserções não são para tratamento de erros, entretanto, assert tenta lidar com o erro abortando. Em vez disso, você não deveria lançar uma exceção e permitir que o chamador trate o erro, se puder? Afinal, o chamador está em uma posição melhor para determinar se a falha de uma função faz com que não valha a pena fazer outra coisa. Talvez o chamador esteja tentando fazer três coisas não relacionadas e ainda possa concluir as outras duas tarefas e simplesmente descartar esta.
daemonspring
E assertestá definido para chamar abort(quando a condição é falsa). Quanto a lançar exceções, não, nem sempre é apropriado. Algumas coisas não podem ser tratadas pelo chamador. O chamador não pode determinar se um bug lógico em uma função de biblioteca de terceiros é recuperável ou se os dados corrompidos podem ser corrigidos.
Jonathan Wakely
6

IMHO, as afirmações são para verificar as condições que, se violadas, tornam todo o resto um absurdo. E, portanto, você não pode se recuperar deles, ou melhor, a recuperação é irrelevante.

Eu os agruparia em 2 categorias:

  • Pecados do desenvolvedor (por exemplo, uma função de probabilidade que retorna valores negativos):

probabilidade de flutuação () {retorno -1,0; }

afirmar (probabilidade ()> = 0,0)

  • A máquina está quebrada (por exemplo, a máquina que executa seu programa está muito errada):

int x = 1;

afirmar (x> 0);

Ambos são exemplos triviais, mas não muito longe da realidade. Por exemplo, pense em algoritmos ingênuos que retornam índices negativos para usar com vetores. Ou programas embutidos em hardware personalizado. Ou melhor, porque sh * t acontece .

E se houver esses erros de desenvolvimento, você não deve estar confiante sobre qualquer mecanismo de recuperação ou tratamento de erros implementado. O mesmo se aplica a erros de hardware.

FranMowinckel
fonte
1
afirmar (probabilidade ()> = 0,0)
Elliott