O comportamento padrão do assert
C ++ é não fazer nada nas compilações de versão. Presumo que isso seja feito por razões de desempenho e talvez para impedir que os usuários vejam mensagens de erro desagradáveis.
No entanto, eu argumentaria que as situações em que um assert
teria disparado mas foi desativado são ainda mais problemáticas, porque o aplicativo provavelmente trava de maneira ainda pior na linha porque alguns invariantes foram quebrados.
Além disso, o argumento de desempenho para mim conta apenas quando é um problema mensurável. Muitos assert
s no meu código não são muito mais complexos do que
assert(ptr != nullptr);
o que terá um pequeno impacto na maioria dos códigos.
Isso me leva à pergunta: As asserções (significando o conceito, não a implementação específica) devem ser ativas nas compilações de versão? Por que não)?
Observe que esta pergunta não é sobre como habilitar afirmações em compilações de versão (como #undef _NDEBUG
ou usando uma implementação de declaração definida). Além disso, não se trata de ativar declarações no código de biblioteca padrão / de terceiros, mas no código controlado por mim.
fonte
Respostas:
O clássico
assert
é uma ferramenta da antiga biblioteca C padrão, não do C ++. Ainda está disponível em C ++, pelo menos por razões de compatibilidade com versões anteriores.Não tenho uma linha do tempo precisa das bibliotecas padrão C disponíveis, mas tenho certeza de que
assert
estava disponível logo após o momento em que o K&R C entrou no ar (por volta de 1978). No C clássico, para escrever programas robustos, a adição de testes de ponteiro NULL e verificação de limites de matriz precisa ser feita com muito mais frequência do que no C ++. O grosso dos testes de ponteiro NULL pode ser evitado usando referências e / ou ponteiros inteligentes em vez de ponteiros, e usando astd::vector
verificação de limites de matriz geralmente é desnecessária. Além disso, o desempenho atingido em 1980 foi definitivamente muito mais importante do que hoje. Portanto, acho que é muito provável que a razão pela qual "assert" tenha sido projetada para estar ativa apenas em compilações de depuração por padrão.Além disso, para tratamento real de erros no código de produção, uma função que apenas testa alguma condição ou invariável e trava o programa se a condição não for atendida, na maioria dos casos não é suficientemente flexível. Para depuração, provavelmente isso é bom, já que quem executa o programa e observa o erro geralmente possui um depurador para analisar o que acontece. Para o código de produção, no entanto, uma solução sensata precisa ser uma função ou mecanismo que
testa alguma condição (e interrompe a execução no escopo em que a condição falha)
fornece uma mensagem de erro clara, caso a condição não se mantenha
permite que o escopo externo receba a mensagem de erro e a envie para um canal de comunicação específico. Esse canal pode ser algo como stderr, um arquivo de log padrão, uma caixa de mensagem em um programa da GUI, um retorno de chamada geral de tratamento de erros, um canal de erro ativado pela rede ou o que melhor se adequar ao software específico.
permite que o escopo externo em uma base por caso decida se o programa deve terminar normalmente ou se deve continuar.
(Obviamente, também existem situações em que terminar o programa imediatamente no caso de uma condição não realizada é a única opção sensata, mas, nesses casos, deve ocorrer também em uma versão de compilação, e não apenas na versão de depuração).
Como o clássico
assert
não fornece esses recursos, não é um bom ajuste para uma versão compilada, assumindo que a versão compilada é o que é implementado na produção.Agora você pode perguntar por que não existe tal função ou mecanismo na lib padrão C, que fornece esse tipo de flexibilidade. Na verdade, em C ++, existe um mecanismo padrão que possui todos esses recursos (e mais), e você sabe disso: é chamado de exceções .
Em C, no entanto, é difícil implementar um bom mecanismo padrão de uso geral para o tratamento de erros com todos os recursos mencionados devido à falta de exceções como parte da linguagem de programação. Portanto, a maioria dos programas C possui seus próprios mecanismos de tratamento de erros com códigos de retorno, ou "goto" s, ou "long jumps", ou uma mistura disso. Geralmente, são soluções pragmáticas que se ajustam ao tipo específico de programa, mas não são "de uso geral o suficiente" para se encaixarem na biblioteca padrão C.
fonte
#include <cassert>
). Isso faz totalmente sentido agora.assert
é a ferramenta errada para isso (lançar uma exceção também pode ser a reação errada) ) No entanto, tenho certeza de que, na realidade,assert
também foi usado para muitos testes em C, onde no C ++ se usaria exceções hoje.Se você deseja que as declarações tenham sido ativadas em uma liberação, solicitou que as declarações fizessem o trabalho errado.
O objetivo das afirmações é que elas não estão ativadas em uma liberação. Isso permite testar invariantes durante o desenvolvimento com código que, de outra forma, teria que ser código de andaime. Código que deve ser removido antes do lançamento.
Se você tem algo que acha que deve ser testado mesmo durante o lançamento, escreva um código que o teste. A
If throw
construção funciona muito bem. Se você deseja dizer algo diferente do que os outros lançamentos, use uma exceção descritiva que diz o que você deseja dizer.Não é que você não possa mudar a maneira como usa declarações. É que isso não lhe proporciona nada de útil, contraria as expectativas e deixa você sem uma maneira limpa de fazer o que as declarações deveriam fazer. Adicione testes que estão inativos em uma liberação.
Por que não relase_assert? Francamente, porque as afirmações não são boas o suficiente para a produção. Sim, há uma necessidade, mas nada preenche bem essa necessidade. Ah, claro que você pode criar o seu próprio. Mecanicamente, sua função throwIf precisa apenas de um bool e uma exceção para lançar. E isso pode atender às suas necessidades. Mas você está realmente limitando o design. É por isso que não me surpreende que não exista um sistema assertivo como exceção na sua biblioteca de idiomas. Certamente não é que você não possa fazer isso. Outros têm . Mas lidar com o caso em que as coisas dão errado é 80% do trabalho para a maioria dos programas. E até agora ninguém nos mostrou um bom tamanho único para todas as soluções. Lidar eficazmente com esses casos pode ser complicado. Se tivéssemos um sistema release_assert enlatado que não atendesse às nossas necessidades, acho que teria feito mais mal do que bem. Você está pedindo uma boa abstração, o que significaria que você não precisaria pensar nesse problema. Também quero um, mas parece que ainda não chegamos.
Por que as declarações são desativadas na versão? As declarações foram criadas no auge da idade do código do andaime. Código que tivemos que remover porque sabia que não o desejávamos na produção, mas sabíamos que queríamos rodar no desenvolvimento para nos ajudar a encontrar bugs. As declarações foram uma alternativa mais limpa ao
if (DEBUG)
padrão que nos deixou deixar o código, mas desativá-lo. Isso foi antes do teste de unidade decolar como a principal maneira de separar o código de teste do código de produção. As declarações ainda são usadas hoje, mesmo por testadores de unidade especializados, tanto para esclarecer as expectativas quanto para cobrir casos em que eles ainda se saem melhor do que os testes de unidade.Por que não deixar apenas o código de depuração em produção? Como o código de produção precisa não envergonhar a empresa, não formatar o disco rígido, não corromper o banco de dados e não enviar e-mails ameaçadores ao presidente. Em suma, é bom poder escrever código de depuração em um local seguro, onde você não precisa se preocupar com isso.
fonte
catch(...)
.if (DEBUG)
para controlar algo diferente de código de depuração. A micro otimização pode não significar nada no seu caso. Mas o idioma não deve ser subvertido apenas porque você não precisa dele.assert
mas do conceito de uma afirmação. Não quero abusarassert
ou confundir os leitores. Eu quis perguntar por que é assim em primeiro lugar. Por que não há maisrelease_assert
? Não há necessidade disso? Qual é a lógica por trás da afirmação de ser desativada na liberação?You're asking for a good abstraction.
Eu não tenho certeza sobre isso. Quero principalmente lidar com questões em que não há recuperação e isso nunca deve acontecer. Leve isso juntoBecause production code needs to [...] not format the hard drive [...]
e eu diria que, nos casos em que os invariantes são quebrados, eu faria uma declaração sobre o UB a qualquer momento.As asserções são uma ferramenta de depuração , não uma técnica de programação defensiva. Se você deseja executar a validação em todas as circunstâncias, execute a validação escrevendo uma condicional - ou crie sua própria macro para reduzir o padrão.
fonte
assert
é uma forma de documentação, como comentários. Como os comentários, você normalmente não os envia aos clientes - eles não pertencem ao código de liberação.Mas o problema com os comentários é que eles podem ficar desatualizados e, no entanto, são deixados. É por isso que as afirmações são boas - elas são verificadas no modo de depuração. Quando a afirmação se torna obsoleta, você a descobre rapidamente e ainda sabe como consertar a afirmação. Esse comentário que ficou desatualizado há 3 anos? Alguém sabe.
fonte
Se você não deseja que uma "declaração" seja desativada, sinta-se à vontade para escrever uma função simples que tenha um efeito semelhante:
Ou seja,
assert
é para testes onde você fazer deseja que eles vão embora no produto enviado. Se você deseja que esse teste faça parte do comportamento definido do programa,assert
é a ferramenta errada.fonte
Não faz sentido argumentar o que a afirmação deve fazer, ela faz o que faz. Se você quiser algo diferente, escreva sua própria função. Por exemplo, tenho Assert que para no depurador ou não faz nada, tenho AssertFatal que irá travar o aplicativo, tenho funções booleanas Asserted e AssertionFailed que afirmam e retornam o resultado para que eu possa afirmar e lidar com a situação.
Para qualquer problema inesperado, você precisa decidir qual é a melhor maneira para o desenvolvedor e o usuário lidar com isso.
fonte
verify(cond)
seria a maneira normal de afirmar e retornar o resultado?Como outros apontaram, esse
assert
é o seu último bastião de defesa contra erros de programadores que nunca devem acontecer. São verificações de sanidade que, esperançosamente, não devem falhar esquerda e direita no momento em que você envia.Ele também foi projetado para ser omitido nas versões de versões estáveis, por qualquer motivo que os desenvolvedores possam achar úteis: estética, desempenho, o que quiserem. É parte do que separa uma compilação de depuração de uma compilação de lançamento e, por definição, uma compilação de lançamento é desprovida de tais asserções. Portanto, há uma subversão do design, se você deseja liberar o analógico "release build com assertions in place", que seria uma tentativa de um release build com uma
_DEBUG
definição de pré - processador e semNDEBUG
definição; não é realmente uma versão mais compilada.O design se estende até a biblioteca padrão. Como um exemplo muito básico entre inúmeras, muitas implementações de uma verificação de sanidade
std::vector::operator[]
irãoassert
garantir que você não esteja verificando o vetor fora dos limites. E a biblioteca padrão começará a ter um desempenho muito pior se você habilitar essas verificações em uma versão. Uma referência devector
usooperator[]
e um preenchedor com essas afirmações incluídas em uma matriz dinâmica antiga simples geralmente mostra que a matriz dinâmica é consideravelmente mais rápida até você desabilitar essas verificações, de modo que elas afetam o desempenho de maneiras distantes e distantes. Uma verificação de ponteiro nulo aqui e uma verificação fora dos limites podem realmente se tornar uma despesa enorme se essas verificações estiverem sendo aplicadas milhões de vezes em todos os quadros em loops críticos que precedem o código, tão simples quanto desreferenciar um ponteiro inteligente ou acessar uma matriz.Portanto, é mais provável que você deseje uma ferramenta diferente para o trabalho e uma que não seja projetada para ser omitida nas compilações de versões, se você quiser que compilações de versões que executam tais verificações de sanidade em áreas-chave. O mais útil que eu pessoalmente acho é o log. Nesse caso, quando um usuário relata um bug, as coisas ficam muito mais fáceis se eles anexam um log e a última linha do log me dá uma grande pista de onde o bug ocorreu e qual pode ser. Em seguida, ao reproduzir suas etapas em uma compilação de depuração, da mesma forma, eu poderia ter uma falha de afirmação, e essa falha de afirmação ainda me dá grandes pistas para otimizar meu tempo. No entanto, como o registro é relativamente caro, não o uso para aplicar verificações de sanidade de nível extremamente baixo, como garantir que uma matriz não seja acessada fora dos limites em uma estrutura de dados genérica.
No entanto, finalmente, e um pouco de acordo com você, eu pude ver um caso razoável em que você pode realmente entregar aos testadores algo semelhante a uma compilação de depuração durante o teste alfa, por exemplo, com um pequeno grupo de testadores alfa que, digamos, assinaram um NDA . Lá, ele pode otimizar o teste alfa se você entregar a seus testadores algo diferente de uma versão completa com algumas informações de depuração anexadas, além de alguns recursos de depuração / desenvolvimento, como testes que eles podem executar e uma saída mais detalhada enquanto executam o software. Eu já vi pelo menos algumas grandes empresas de jogos fazendo coisas assim para o alfa. Mas isso é algo como testes alfa ou internos, nos quais você está realmente tentando dar aos testadores algo diferente de uma versão compilada. Se você está realmente tentando enviar uma versão compilada, por definição, não deve ter
_DEBUG
definido ou isso realmente está confundindo a diferença entre uma compilação "debug" e "release".Como indicado acima, as verificações não são necessariamente triviais do ponto de vista de desempenho. Muitos são provavelmente triviais, mas, novamente, até a biblioteca padrão os usa e isso pode afetar o desempenho de maneiras inaceitáveis para muitas pessoas em muitos casos, se, digamos, a passagem de acesso aleatório
std::vector
demorar 4 vezes mais do que deveria ser uma versão otimizada do build por causa de sua verificação de limites que nunca deveria falhar.Em uma equipe anterior, na verdade, tivemos que fazer com que nossa biblioteca de matrizes e vetores excluísse algumas afirmações em certos caminhos críticos, apenas para acelerar a compilação de depuração, porque essas afirmações estavam diminuindo as operações matemáticas em uma ordem de grandeza até o ponto em que estavam. começando a exigir que esperemos 15 minutos antes que pudéssemos rastrear o código de interesse. Meus colegas queriam apenas remover o
asserts
diretamente porque descobriram que apenas fazer isso fazia uma diferença enorme. Em vez disso, decidimos apenas fazer com que os caminhos críticos de depuração os evitassem. Quando fizemos esses caminhos críticos usarem os dados vetoriais / matriz diretamente, sem passar pela verificação de limites, o tempo necessário para executar a operação completa (que incluía mais do que apenas matemática vetorial / matriz) reduzido de minutos para segundos. Portanto, esse é um caso extremo, mas definitivamente as afirmações nem sempre são insignificantes do ponto de vista de desempenho, nem mesmo próximas.Mas também é assim que
asserts
são projetados. Se eles não tiveram um impacto de desempenho tão grande em todos os aspectos, talvez eu seja a favor se eles foram projetados como mais do que um recurso de compilação de depuração ou podemos usar ovector::at
que inclui a verificação de limites, mesmo em versões de lançamento e lançamentos fora dos limites acesso, por exemplo (ainda com um enorme impacto no desempenho). Mas atualmente acho o design deles muito mais útil, dado o enorme impacto no desempenho dos meus casos, como um recurso somente de depuração, que é omitido quandoNDEBUG
definido. Para os casos com os quais trabalhei, pelo menos, faz uma diferença enorme a criação de uma versão excluir as verificações de sanidade que nunca deveriam realmente falhar em primeiro lugar.vector::at
vs.vector::operator[]
Eu acho que a distinção desses dois métodos está no centro disso e da alternativa: exceções.
vector::operator[]
implementações normalmenteassert
para garantir que o acesso fora dos limites acione um erro facilmente reproduzível ao tentar acessar um vetor fora dos limites. Mas os implementadores da biblioteca fazem isso com o pressuposto de que não custará um centavo em uma compilação de versão otimizada.Enquanto isso,
vector::at
é fornecido o que sempre verifica e lança fora dos limites, mesmo nas compilações de versão, mas tem uma penalidade de desempenho até o ponto em que geralmente vejo muito mais código usando dovector::operator[]
quevector::at
. Muito do design do C ++ faz eco à idéia de "pagar pelo que você usa / precisa", e muitas pessoas são a favoroperator[]
, o que nem se importa com a verificação de limites nas versões de lançamento, com base na noção de que eles não não precisa verificar os limites em suas compilações de versão otimizadas. De repente, se as asserções fossem ativadas nas compilações de versão, o desempenho dessas duas seria idêntico e o uso do vetor sempre acabaria sendo mais lento que um array dinâmico. Portanto, grande parte do design e dos benefícios das asserções é baseada na idéia de que elas se tornam livres em uma versão compilada.release_assert
Isso é interessante depois de descobrir essas intenções. Naturalmente, os casos de uso de todos seriam diferentes, mas acho que encontraria algum uso para um
release_assert
que faça a verificação e trava o software, mostrando um número de linha e uma mensagem de erro, mesmo nas versões lançadas.Seria para alguns casos obscuros no meu caso que eu não quero que o software se recupere normalmente, como faria se uma exceção fosse lançada. Eu gostaria que ele falhasse mesmo na versão nesses casos, para que o usuário possa receber um número de linha para relatar quando o software encontrou algo que nunca deveria acontecer, ainda no campo das verificações de sanidade quanto a erros do programador, não erros externos de entrada como exceções, mas barato o suficiente para ser feito sem se preocupar com seu custo no lançamento.
Na verdade, existem alguns casos em que eu considerava uma falha grave com um número de linha e uma mensagem de erro preferíveis à recuperação normal de uma exceção lançada que pode ser barata o suficiente para manter em uma versão. E há alguns casos em que é impossível recuperar de uma exceção, como um erro encontrado ao tentar recuperar de uma existente. Lá, eu encontraria um ajuste perfeito para,
release_assert(!"This should never, ever happen! The software failed to fail!");
naturalmente, que seria muito barato, já que a verificação seria realizada dentro de um caminho excepcional em primeiro lugar e não custaria nada nos caminhos normais de execução.fonte
Additionally, the performance argument for me only counts when it is a measurable problem.
Portanto, a idéia é decidir, caso a caso, quando tirar uma afirmação do release.assert
que se baseia nos mesmos_DEBUG/NDEBUG
padrões de pré-processador que o que você usaria ao usar aassert
macro. Portanto, não há como habilitar seletivamente asserções apenas para um trecho de código sem habilitá-lo também para a lib padrão, por exemplo, supondo que ele use a lib padrão.assert
.vector::operator[]
evector::at
, por exemplo, teria praticamente o mesmo desempenho se as asserções estivessem ativadas nas compilações de versão, e não haveria sentido em usaroperator[]
mais do ponto de vista de desempenho, pois, de qualquer maneira, ele faria a verificação de limites.Estou codificando em Ruby, não em C / C ++ e, portanto, não falarei sobre diferenças entre declarações e exceções, mas gostaria de falar sobre isso como algo que interrompe o tempo de execução . Eu discordo da maioria das respostas acima, pois interromper o programa com a impressão de um backtrace funciona bem para mim como uma técnica de programação defensiva.
Se existe uma maneira de afirmar a rotina (absolutamente, independentemente de como é sintaticamente escrita e se a palavra "afirmar" é usada ou existe na linguagem de programação ou no dsl), isso significa que algum trabalho deve ser feito e o produto imediatamente volta do "liberado, pronto para o uso" para "precisa de correção" - agora reescreva-o para tratamento de exceções reais ou corrija um bug que causou a exibição de dados incorretos.
Quero dizer, afirmar não é algo com o qual você deve conviver e ligar com frequência - é um sinal de parada que indica que você deve fazer algo para que isso nunca aconteça novamente. E dizer que "release build não deve ter afirmações" é como dizer "release build não deve ter bugs" - cara, é quase impossível, lidar com isso.
Ou pense neles como "falhas de unittests feitas e executadas pelo usuário final". Você não pode prever tudo o que o usuário fará com o seu programa, mas se algo muito sério der errado, ele deverá parar - isso é semelhante à maneira como você constrói pipelines de construção - você interrompe o processo e não publica, não é? ? A afirmação força o usuário a parar, relatar e aguardar sua ajuda.
fonte