Eu sou um grande fã de escrever assert
verificações no código C ++ como uma maneira de capturar casos durante o desenvolvimento que não podem acontecer, mas acontecem devido a erros de lógica no meu programa. Esta é uma boa prática em geral.
No entanto, notei que algumas funções que escrevo (que fazem parte de uma classe complexa) têm mais de 5 afirmações, o que parece potencialmente uma má prática de programação, em termos de legibilidade e manutenção. Eu acho que ainda é ótimo, pois cada um exige que eu pense nas condições pré e pós-funções e elas realmente ajudam a detectar bugs. No entanto, eu só queria divulgar isso para perguntar se existem paradigmas melhores para detectar erros de lógica nos casos em que é necessário um grande número de verificações.
Comentário do Emacs : Como o Emacs é meu IDE de escolha, eu tenho um pouco de cinza as declarações de asserção, que ajudam a reduzir a sensação de desorganização que elas podem proporcionar. Aqui está o que eu adiciono ao meu arquivo .emacs:
; gray out the "assert(...)" wrapper
(add-hook 'c-mode-common-hook
(lambda () (font-lock-add-keywords nil
'(("\\<\\(assert\(.*\);\\)" 1 '(:foreground "#444444") t)))))
; gray out the stuff inside parenthesis with a slightly lighter color
(add-hook 'c-mode-common-hook
(lambda () (font-lock-add-keywords nil
'(("\\<assert\\(\(.*\);\\)" 1 '(:foreground "#666666") t)))))
fonte
Respostas:
Eu já vi centenas de erros que teriam sido resolvidos mais rapidamente se alguém tivesse escrito mais declarações, e nenhum que teria sido resolvido mais rapidamente escrevendo menos .
Talvez a legibilidade possa ser um problema - embora tenha sido minha experiência que as pessoas que escrevem boas afirmações também escrevem código legível. E nunca me incomoda ver o início de uma função começar com um bloco de afirmações para verificar se os argumentos não são lixo - basta colocar uma linha em branco após ela.
Também na minha experiência, a manutenção é sempre aprimorada por afirmações, assim como por testes de unidade. As declarações fornecem uma verificação de integridade de que o código está sendo usado da maneira que ele deveria ser usado.
fonte
Bem, claro que é. [Imagine um exemplo desagradável aqui.] No entanto, ao aplicar as diretrizes detalhadas a seguir, você não deve ter problemas para empurrar esse limite na prática. Também sou um grande fã de afirmações e as uso de acordo com esses princípios. Muitos desses conselhos não são especiais para afirmações, mas apenas boas práticas gerais de engenharia aplicadas a elas.
Lembre-se do tempo de execução e da sobrecarga binária
As asserções são ótimas, mas se elas tornarem seu programa inaceitavelmente lento, será muito irritante ou você as desativará mais cedo ou mais tarde.
Gosto de avaliar o custo de uma asserção em relação ao custo da função em que ela está contida. Considere os dois exemplos a seguir.
A função em si é uma operação O (1), mas as asserções são responsáveis pela sobrecarga de O ( n ). Não acho que você gostaria que essas verificações estivessem ativas, a menos que em circunstâncias muito especiais.
Aqui está outra função com asserções semelhantes.
A função em si é uma operação O ( n ), portanto, é muito menos adicionar uma sobrecarga O ( n ) adicional para a asserção. Diminuir a velocidade de uma função por um fator constante pequeno (nesse caso, provavelmente menor que 3) é algo que geralmente podemos pagar em uma compilação de depuração, mas talvez não em uma compilação de versão.
Agora considere este exemplo.
Embora muitas pessoas provavelmente se sintam muito mais confortáveis com essa asserção O (1) do que com as duas asserções O ( n ) do exemplo anterior, elas são moralmente equivalentes na minha opinião. Cada um deles adiciona uma sobrecarga na ordem da complexidade da própria função.
Finalmente, existem as afirmações "realmente baratas" que são dominadas pela complexidade da função em que estão contidas.
Aqui, temos duas asserções O (1) em uma função O ( n ). Provavelmente não será um problema manter essa sobrecarga, mesmo nas versões de lançamento.
No entanto, lembre-se de que as complexidades assintóticas nem sempre fornecem uma estimativa adequada porque, na prática, estamos sempre lidando com tamanhos de entrada limitados por alguns fatores constantes e constantes finitos ocultos por "Big- O ", que podem muito bem não ser desprezíveis.
Então, agora que identificamos cenários diferentes, o que podemos fazer sobre eles? Uma abordagem (provavelmente também) fácil seria seguir uma regra como "Não use asserções que dominam a função em que estão contidas". Embora possa funcionar para alguns projetos, outros podem precisar de uma abordagem mais diferenciada. Isso pode ser feito usando macros de asserção diferentes para os diferentes casos.
Agora você pode usar as três macros e
MY_ASSERT_LOW
, em vez da macro "tamanho único" da biblioteca padrão, para afirmações que são dominadas por, nem dominadas por, nem dominando e dominando, respectivamente, a complexidade de sua função de contenção. Ao criar o software, você pode pré-definir o símbolo do pré-processador para selecionar que tipo de asserções devem entrar no executável. As constantes e não correspondem a nenhuma macro de asserção e devem ser usadas como valores para ativar ou desativar todas as asserções, respectivamente.MY_ASSERT_MEDIUM
MY_ASSERT_HIGH
assert
MY_ASSERT_COST_LIMIT
MY_ASSERT_COST_NONE
MY_ASSERT_COST_ALL
MY_ASSERT_COST_LIMIT
Estamos contando com a suposição aqui de que um bom compilador não irá gerar nenhum código para
e transformar
para dentro
o que acredito ser uma suposição segura hoje em dia.
Se você estiver prestes a ajustar o código acima, considere anotações específicas do compilador, como
__attribute__ ((cold))
onmy::assertion_failed
ou__builtin_expect(…, false)
on,!(CONDITION)
para reduzir a sobrecarga das asserções passadas. Nas compilações de versão, você também pode substituir a chamada de funçãomy::assertion_failed
por algo como__builtin_trap
reduzir a pegada com o inconveniente de perder uma mensagem de diagnóstico.Esses tipos de otimizações são realmente relevantes apenas em asserções extremamente baratas (como comparar dois números inteiros que já são apresentados como argumentos) em uma função que é muito compacta, sem considerar o tamanho adicional do binário acumulado ao incorporar todas as sequências de mensagens.
Compare como esse código
é compilado no seguinte assembly
enquanto o código a seguir
dá essa montagem
com o qual me sinto muito mais confortável. (Os exemplos foram testados com o GCC 5.3.0 usando os sinalizadores
-std=c++14
,-O3
e-march=native
4.3.3-2-ARCH x86_64 GNU / Linux. Não são mostrados nos trechos acima as declaraçõestest::positive_difference_1st
e astest::positive_difference_2nd
quais eu adicionei o__attribute__ ((hot))
.my::assertion_failed
Foi declarado__attribute__ ((cold))
.)Declarar pré-condições na função que depende delas
Suponha que você tenha a seguinte função com o contrato especificado.
Em vez de escrever
em cada local de chamada, coloque essa lógica uma vez na definição de
count_letters
e chamá-lo sem mais delongas.
Isso tem as seguintes vantagens.
assert
instruções no seu código.A desvantagem óbvia é que você não receberá o local de origem do site de chamada na mensagem de diagnóstico. Eu acredito que este é um problema menor. Um bom depurador deve permitir rastrear a origem da violação do contrato de maneira conveniente.
O mesmo pensamento se aplica às funções "especiais", como operadores sobrecarregados. Quando estou escrevendo iteradores, normalmente - se a natureza do iterador permitir - dou a eles uma função de membro
que permite perguntar se é seguro desreferenciar o iterador. (Obviamente, na prática, quase sempre é possível garantir que não será seguro desreferenciar o iterador. Mas acredito que você ainda pode detectar muitos bugs com essa função.) Em vez de desarrumar todo o meu código que usa o iterador com
assert(iter.good())
instruções, prefiro colocar uma únicaassert(this->good())
como a primeira linha dooperator*
na implementação do iterador.Se você estiver usando a biblioteca padrão, em vez de afirmar manualmente suas pré-condições no seu código-fonte, ative suas verificações nas construções de depuração. Eles podem fazer verificações ainda mais sofisticadas, como testar se o contêiner ao qual um iterador se refere ainda existe. (Consulte a documentação para libstdc ++ e libc ++ (trabalho em andamento) para obter mais informações.)
Fatore condições comuns
Suponha que você esteja escrevendo um pacote de álgebra linear. Muitas funções terão pré-condições complicadas e violá-las frequentemente causará resultados errados que não são imediatamente reconhecíveis como tais. Seria muito bom se essas funções afirmassem suas pré-condições. Se você definir vários predicados que informam certas propriedades sobre uma estrutura, essas asserções se tornarão muito mais legíveis.
Também fornecerá mensagens de erro mais úteis.
ajuda muito mais do que, digamos
onde você primeiro deve procurar o código-fonte no contexto para descobrir o que foi realmente testado.
Se você possui
class
invariantes não triviais, provavelmente é uma boa ideia afirmá-los de vez em quando quando você mexe com o estado interno e deseja garantir que você esteja deixando o objeto em um estado válido no retorno.Para esse propósito, achei útil definir uma
private
função de membro que eu chamo convencionalmenteclass_invaraiants_hold_
. Suponha que você estava reimplementandostd::vector
(Como todos sabemos que não é bom o suficiente.), Ele pode ter uma função como essa.Observe algumas coisas sobre isso.
const
enoexcept
, de acordo com a diretriz de que afirmações não devem ter efeitos colaterais. Se fizer sentido, também declareconstexpr
.assert(this->class_invariants_hold_())
. Dessa forma, se as asserções forem compiladas, podemos ter certeza de que não haverá custo adicional no tempo de execução.if
instruções comreturn
s iniciais, em vez de uma expressão grande. Isso facilita percorrer a função em um depurador e descobrir qual parte da invariante foi quebrada se a asserção for acionada.Não afirme coisas tolas
Algumas coisas simplesmente não fazem sentido afirmar.
Essas afirmações não tornam o código nem um pouco mais legível ou mais fácil de raciocinar. Todo programador de C ++ deve ter certeza de como
std::vector
funciona para garantir que o código acima esteja correto simplesmente olhando para ele. Não estou dizendo que você nunca deve afirmar o tamanho de um contêiner. Se você adicionou ou removeu elementos usando algum fluxo de controle não trivial, essa afirmação pode ser útil. Mas se apenas repetir o que foi escrito no código de não afirmação logo acima, não haverá valor agregado.Além disso, não afirme que as funções da biblioteca funcionam corretamente.
Se você confia pouco na biblioteca, é melhor considerar usar outra biblioteca.
Por outro lado, se a documentação da biblioteca não estiver 100% clara e você ganhar confiança sobre seus contratos lendo o código-fonte, faz muito sentido afirmar sobre esse "contrato inferido". Se estiver quebrado em uma versão futura da biblioteca, você notará rapidamente.
É melhor que a solução a seguir, que não informa se suas suposições estavam corretas.
Não abuse das asserções para implementar a lógica do programa
As asserções devem ser usadas apenas para descobrir bugs que valham a pena matar imediatamente seu aplicativo. Eles não devem ser usados para verificar qualquer outra condição, mesmo se a reação apropriada a essa condição também for parar imediatamente.
Portanto, escreva isso…
…ao invés disso.
Também nunca use asserções para validar entrada não confiável ou verificar
std::malloc
sereturn
você nãonullptr
. Mesmo se você souber que nunca desativará as asserções, mesmo nas versões de lançamento, uma asserção comunica ao leitor que verifica algo que é sempre verdadeiro, uma vez que o programa é livre de erros e não apresenta efeitos colaterais visíveis. Se esse não for o tipo de mensagem que você deseja comunicar, use um mecanismo alternativo de tratamento de erros, comothrow
uma exceção. Se você achar conveniente ter um wrapper de macro para suas verificações sem asserções, continue escrevendo um. Apenas não chame isso de "afirmar", "assumir", "exigir", "garantir" ou algo assim. Sua lógica interna pode ser a mesma que paraassert
, exceto que nunca é compilada, é claro.Mais Informações
Eu encontrei talk John Lakos' Programação Defensiva feito direito , dado no CppCon'14 ( 1 st parte , 2 nd parte ) muito esclarecedor. Ele adota a ideia de personalizar quais asserções estão habilitadas e como reagir a exceções com falha ainda mais do que eu fiz nesta resposta.
fonte
Assertions are great, but ... you will turn them off sooner or later.
- Espero que mais cedo, como antes do código ser enviado. As coisas que precisam fazer o programa morrer na produção devem fazer parte do código "real", não de afirmações.Acho que, com o tempo, escrevo menos declarações, porque muitas delas equivalem a "o compilador está funcionando" e "a biblioteca está funcionando". Depois que você começar a pensar no que exatamente está testando, suspeito que escreverá menos declarações.
Por exemplo, um método que (digamos) adiciona algo a uma coleção não deve precisar afirmar que a coleção existe - isso geralmente é uma pré-condição da classe que possui a mensagem ou é um erro fatal que deve retornar ao usuário . Portanto, verifique uma vez, muito cedo, e assuma.
As asserções para mim são uma ferramenta de depuração, e geralmente as usarei de duas maneiras: encontrar um bug na minha mesa (e elas não são verificadas. Bem, talvez a única chave que possa ser); e encontrar um bug na mesa do cliente (e eles são verificados). Nas duas vezes, estou usando asserções principalmente para gerar um rastreamento de pilha depois de forçar uma exceção o mais cedo possível. Esteja ciente de que asserções usadas dessa maneira podem facilmente levar a heisenbugs - o bug pode nunca ocorrer na compilação de depuração que possui as asserções ativadas.
fonte
Poucas afirmações: boa sorte em mudar esse código cheio de suposições ocultas.
Demasiadas afirmações: pode levar a problemas de legibilidade e possível cheiro de código - a classe, função, API foi projetada corretamente quando há tantas suposições colocadas nas declarações de asserção?
Também pode haver afirmações que realmente não verificam nada ou verificam coisas como configurações do compilador em cada função: /
Apontar para o ponto ideal, mas não menos (como alguém já disse, "mais" de afirmações é menos prejudicial do que ter muito poucos ou que Deus nos ajude - nenhum).
fonte
Seria incrível se você pudesse escrever uma função Assert que fizesse apenas uma referência a um método CONST booleano. Dessa forma, você tem certeza de que suas declarações não têm efeitos colaterais, garantindo que um método const booleano seja usado para testar a declaração
tiraria um pouco da legibilidade, especialmente porque acho que você não pode anotar um lambda (em c ++ 0x) para ser uma const para alguma classe, o que significa que você não pode usar lambdas para isso
exagero se você me perguntar, mas se eu começar a ver um certo nível de poluição devido a afirmações, eu seria cauteloso em duas coisas:
fonte
Eu escrevi em C # muito mais do que em C ++, mas as duas linguagens não estão muito distantes. No .Net, eu uso Asserts para condições que não devem acontecer, mas também muitas vezes lanço exceções quando não há como continuar. O depurador do VS2010 me mostra muitas informações boas sobre uma exceção, independentemente da otimização da versão. Também é uma boa ideia adicionar testes de unidade, se puder. Às vezes, o log também é uma boa coisa para ter como auxílio de depuração.
Então, pode haver muitas afirmações? Sim. Escolher entre Abortar / Ignorar / Continuar 15 vezes em um minuto fica irritante. Uma exceção é lançada apenas uma vez. É difícil quantificar o ponto em que há muitas afirmações, mas se suas afirmações cumprem o papel de afirmações, exceções, testes de unidade e registro, algo está errado.
Eu reservaria afirmações para os cenários que não deveriam acontecer. Você pode exagerar na declaração inicialmente, porque as afirmações são mais rápidas de escrever, mas redefine o código mais tarde - transforme algumas em exceções, outras em testes etc. Se você tiver disciplina suficiente para limpar todos os comentários do TODO, deixe um comente ao lado de cada um que planeja retrabalhar e NÃO ESQUEÇA de abordar o TODO posteriormente.
fonte
Eu quero trabalhar com você! Alguém que escreve muito
asserts
é fantástico. Não sei se existe algo como "muitos". Muito mais comuns para mim são as pessoas que escrevem muito pouco e acabam encontrando ocasionalmente uma questão mortal do UB, que só aparece na lua cheia, que poderia ter sido facilmente reproduzida repetidamente com um simplesassert
.Mensagem de falha
A única coisa em que consigo pensar é incorporar informações de falha ao
assert
se você ainda não o estiver fazendo, assim:Dessa forma, você pode não sentir mais como se tivesse muitos, se já não estivesse fazendo isso, pois agora você está fazendo com que suas declarações tenham um papel mais forte na documentação de suposições e pré-condições.
Efeitos colaterais
Obviamente,
assert
pode realmente ser mal utilizado e introduzir erros, como:... se
foo()
desencadeia efeitos colaterais, então você deve ter muito cuidado com isso, mas tenho certeza de que você já é alguém que afirma de maneira muito liberal (uma "pessoa experiente"). Esperamos que seu procedimento de teste também seja tão bom quanto sua atenção cuidadosa para afirmar suposições.Velocidade de depuração
Embora a velocidade da depuração geralmente esteja no final da nossa lista de prioridades, uma vez eu acabei afirmando muito em uma base de código antes de executar a compilação de depuração no depurador era 100 vezes mais lento que o lançamento.
Foi principalmente porque eu tinha funções como esta:
... onde cada chamada
operator[]
faria uma afirmação de verificação de limites. Acabei substituindo alguns críticos críticos de desempenho por equivalentes inseguros que não afirmam apenas acelerar a compilação de depuração drasticamente a um custo menor para apenas a segurança no nível de detalhe da implementação e apenas porque a velocidade atingida estava começando degradar muito visivelmente a produtividade (fazer com que o benefício de uma depuração mais rápida supere o custo de perder algumas afirmações, mas apenas para funções como essa função de produto cruzado que estava sendo usada nos caminhos mais críticos e medidos, e nãooperator[]
em geral).Princípio da responsabilidade única
Embora eu não ache que você possa realmente dar errado com mais afirmações (pelo menos é muito, muito melhor errar do lado de muitas do que de poucas), as afirmações em si podem não ser um problema, mas podem estar indicando um.
Se você tiver 5 asserções para uma única chamada de função, por exemplo, pode estar fazendo muito. Sua interface pode ter muitas condições prévias e parâmetros de entrada, por exemplo, considero que não está relacionado apenas ao tópico do que constitui um número saudável de asserções (para as quais eu geralmente responderia: "quanto mais, melhor!"), Mas isso pode ser uma possível bandeira vermelha (ou muito possivelmente não).
fonte
É muito razoável adicionar verificações ao seu código. Para declaração simples (aquela incorporada no compilador C e C ++), meu padrão de uso é que uma declaração com falha significa que há um erro no código que precisa ser corrigido. Eu interpreto isso um pouco generosamente; se eu esperar uma solicitação da web para retornar um status 200 e assert para ele, sem manipulação outros casos, em seguida, uma afirmação que falhou é que de fato mostram um bug no meu código, de modo que o assert é justificada.
Portanto, quando as pessoas afirmam que apenas verifica o que o código faz é supérfluo, isso não está certo. Essa afirmação verifica o que eles acham que o código faz, e o objetivo da afirmação é verificar se a suposição de que não há erro no código está correta. E a afirmação também pode servir como documentação. Se eu assumir que após executar um loop i == n e não for 100% óbvio a partir do código, "assert (i == n)" será útil.
É melhor ter mais do que apenas "afirmar" em seu repertório para lidar com situações diferentes. Por exemplo, a situação em que verifico que algo não acontece indica um erro, mas ainda assim continuo trabalhando em torno dessa condição. (Por exemplo, se eu usar algum cache, posso verificar se há erros, e se ocorrer um erro inesperado, pode ser seguro corrigi-lo jogando o cache fora. Quero algo que seja quase uma afirmação, que seja informado durante o desenvolvimento e ainda me permite continuar.
Outro exemplo é a situação em que não espero que algo aconteça, tenho uma solução genérica, mas se isso acontecer, quero saber sobre isso e examiná-lo. Novamente, algo quase como uma afirmação, que deveria me dizer durante o desenvolvimento. Mas não é bem uma afirmação.
Declarações em excesso: se uma declaração travar seu programa quando estiver nas mãos do usuário, você não deverá ter nenhuma declaração que trava por causa de falsos negativos.
fonte
Depende. Se os requisitos de código estiverem claramente documentados, a asserção deve sempre corresponder aos requisitos. Nesse caso, é uma coisa boa. No entanto, se não houver requisitos ou requisitos mal escritos, seria difícil para os novos programadores editar o código sem precisar se referir ao teste de unidade a cada vez para descobrir quais são os requisitos.
fonte