É possível escrever muitas afirmações?


33

Eu sou um grande fã de escrever assertverificaçõ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)))))

3
Devo admitir que esta é uma pergunta que passou pela minha cabeça de vez em quando. Interessado em ouvir as opiniões dos outros sobre isso.
Capitão Sensible

Respostas:


45

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 .

[C] deveria [muitas afirmações] potencialmente ser uma má prática de programação, em termos de legibilidade e manutenção [?]

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.


1
Boa resposta. Também adicionei uma descrição à questão de como eu melhoro a legibilidade com o Emacs.
Alan Turing

2
"tem sido minha experiência que as pessoas que escrevem boas afirmações também escrevem código legível" << ponto excelente. Tornar o código legível depende tanto do programador quanto das técnicas que ele ou ela é e não tem permissão para usar. Vi boas técnicas se tornarem ilegíveis nas mãos erradas, e até o que muitos considerariam más técnicas se tornaria perfeitamente claro, até elegante, pelo uso adequado da abstração e dos comentários.
Greg Jackson

Eu tive algumas falhas de aplicativos causadas por afirmações erradas. Então eu vi bugs que não existiriam se alguém (eu) tivesse escrito menos declarações.
CodesInChaos

@CodesInChaos Indiscutivelmente, erros de digitação, isso aponta para um erro na formulação do problema - ou seja, o bug estava no design, daí a incompatibilidade entre afirmações e (outros) códigos.
Lawrence

12

É possível escrever muitas afirmações?

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.

// Precondition:  queue is not empty
// Invariant:     queue is sorted
template <typename T>
const T&
sorted_queue<T>::max() const noexcept
{
  assert(!this->data_.empty());
  assert(std::is_sorted(std::cbegin(this->data_), std::cend(this->data_)));
  return this->data_.back();
}

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.

// Requirement:   op : T -> T is monotonic [ie x <= y implies op(x) <= op(y)]
// Invariant:     queue is sorted
// Postcondition: each item x in the queue is replaced by op(x)
template <typename T>
template <typename FuncT>
void
sorted_queue<T>::apply_monotonic_function(FuncT&& op)
{
  assert(std::is_sorted(std::cbegin(this->data_), std::cend(this->data_)));
  std::transform(std::cbegin(this->data_), std::cend(this->data_),
                 std::begin(this->data_), std::forward<FuncT>(op));
  assert(std::is_sorted(std::cbegin(this->data_), std::cend(this->data_)));
}

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.

// Precondition:  queue is not empty
// Invariant:     queue is sorted
// Postcondition: last element is removed from queue
template <typename T>
void
sorted_queue<T>::pop_back() noexcept
{
  assert(!this->data_.empty());
  return this->data_.pop_back();
}

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.

// Requirement:   cmp : T x T -> bool is a strict weak ordering
// Precondition:  queue is not empty
// Postcondition: if x is returned, then there is no y in the queue
//                such that cmp(x, y)
template <typename T>
template <typename CmpT>
const T&
sorted_queue<T>::max(CmpT&& cmp) const
{
  assert(!this->data_.empty());
  const auto pos = std::max_element(std::cbegin(this->data_),
                                    std::cend(this->data_),
                                    std::forward<CmpT>(cmp));
  assert(pos != std::cend(this->data_));
  return *pos;
}

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.

#define MY_ASSERT_IMPL(COST, CONDITION)                                       \
  (                                                                           \
    ( ((COST) <= (MY_ASSERT_COST_LIMIT)) && !(CONDITION) )                    \
      ? ::my::assertion_failed(__FILE__, __LINE__, __FUNCTION__, # CONDITION) \
      : (void) 0                                                              \
  )

#define MY_ASSERT_LOW(CONDITION)                                              \
  MY_ASSERT_IMPL(MY_ASSERT_COST_LOW, CONDITION)

#define MY_ASSERT_MEDIUM(CONDITION)                                           \
  MY_ASSERT_IMPL(MY_ASSERT_COST_MEDIUM, CONDITION)

#define MY_ASSERT_HIGH(CONDITION)                                             \
  MY_ASSERT_IMPL(MY_ASSERT_COST_HIGH, CONDITION)

#define MY_ASSERT_COST_NONE    0
#define MY_ASSERT_COST_LOW     1
#define MY_ASSERT_COST_MEDIUM  2
#define MY_ASSERT_COST_HIGH    3
#define MY_ASSERT_COST_ALL    10

#ifndef MY_ASSERT_COST_LIMIT
#  define MY_ASSERT_COST_LIMIT MY_ASSERT_COST_MEDIUM
#endif

namespace my
{

  [[noreturn]] extern void
  assertion_failed(const char * filename, int line, const char * function,
                   const char * message) noexcept;

}

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_MEDIUMMY_ASSERT_HIGHassertMY_ASSERT_COST_LIMITMY_ASSERT_COST_NONEMY_ASSERT_COST_ALLMY_ASSERT_COST_LIMIT

Estamos contando com a suposição aqui de que um bom compilador não irá gerar nenhum código para

if (false_constant_expression && run_time_expression) { /* ... */ }

e transformar

if (true_constant_expression && run_time_expression) { /* ... */ }

para dentro

if (run_time_expression) { /* ... */ }

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))on my::assertion_failedou __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ção my::assertion_failedpor algo como __builtin_trapreduzir 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

int
positive_difference_1st(const int a, const int b) noexcept
{
  if (!(a > b))
    my::assertion_failed(__FILE__, __LINE__, __FUNCTION__, "!(a > b)");
  return a - b;
}

é compilado no seguinte assembly

_ZN4test23positive_difference_1stEii:
.LFB0:
        .cfi_startproc
        cmpl    %esi, %edi
        jle     .L5
        movl    %edi, %eax
        subl    %esi, %eax
        ret
.L5:
        subq    $8, %rsp
        .cfi_def_cfa_offset 16
        movl    $.LC0, %ecx
        movl    $_ZZN4test23positive_difference_1stEiiE12__FUNCTION__, %edx
        movl    $50, %esi
        movl    $.LC1, %edi
        call    _ZN2my16assertion_failedEPKciS1_S1_
        .cfi_endproc
.LFE0:

enquanto o código a seguir

int
positive_difference_2nd(const int a, const int b) noexcept
{
  if (__builtin_expect(!(a > b), false))
    __builtin_trap();
  return a - b;
}

dá essa montagem

_ZN4test23positive_difference_2ndEii:
.LFB1:
        .cfi_startproc
        cmpl    %esi, %edi
        jle     .L8
        movl    %edi, %eax
        subl    %esi, %eax
        ret
        .p2align 4,,7
        .p2align 3
.L8:
        ud2
        .cfi_endproc
.LFE1:

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, -O3e -march=native4.3.3-2-ARCH x86_64 GNU / Linux. Não são mostrados nos trechos acima as declarações test::positive_difference_1ste as test::positive_difference_2ndquais eu adicionei o __attribute__ ((hot)). my::assertion_failedFoi 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.

/**
 * @brief
 *         Counts the frequency of a letter in a string.
 *
 * The frequency count is case-insensitive.
 *
 * If `text` does not point to a NUL terminated character array or `letter`
 * is not in the character range `[A-Za-z]`, the behavior is undefined.
 *
 * @param text
 *         text to count the letters in
 *
 * @param letter
 *         letter to count
 *
 * @returns
 *         occurences of `letter` in `text`
 *
 */
std::size_t
count_letters(const char * text, int letter) noexcept;

Em vez de escrever

assert(text != nullptr);
assert((letter >= 'A' && letter <= 'Z') || (letter >= 'a' && letter <= 'z'));
const auto frequency = count_letters(text, letter);

em cada local de chamada, coloque essa lógica uma vez na definição de count_letters

std::size_t
count_letters(const char *const text, const int letter) noexcept
{
  assert(text != nullptr);
  assert((letter >= 'A' && letter <= 'Z') || (letter >= 'a' && letter <= 'z'));
  auto frequency = std::size_t {};
  // TODO: Figure this out...
  return frequency;
}

e chamá-lo sem mais delongas.

const auto frequency = count_letters(text, letter);

Isso tem as seguintes vantagens.

  • Você só precisa escrever o código de asserção uma vez. Como o objetivo das funções é que elas sejam chamadas - geralmente mais de uma vez - isso deve reduzir o número geral de assertinstruções no seu código.
  • Ele mantém a lógica que verifica as pré-condições próximas à lógica que depende delas. Eu acho que esse é o aspecto mais importante. Se seus clientes usam mal sua interface, eles também não podem aplicar as asserções corretamente, portanto é melhor que a função informe a eles.

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

bool
good() const noexcept;

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 única assert(this->good())como a primeira linha do operator*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.

template <typename MatrixT>
auto
cholesky_decompose(MatrixT&& m)
{
  assert(is_square(m) && is_symmetric(m));
  // TODO: Somehow decompose that thing...
}

Também fornecerá mensagens de erro mais úteis.

cholesky.hxx:357: cholesky_decompose: assertion failed: is_symmetric(m)

ajuda muito mais do que, digamos

detail/basic_ops.hxx:1289: fast_compare: assertion failed: m(i, j) == m(j, i)

onde você primeiro deve procurar o código-fonte no contexto para descobrir o que foi realmente testado.

Se você possui classinvariantes 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 privatefunção de membro que eu chamo convencionalmente class_invaraiants_hold_. Suponha que você estava reimplementando std::vector(Como todos sabemos que não é bom o suficiente.), Ele pode ter uma função como essa.

template <typename T>
bool
vector<T>::class_invariants_hold_() const noexcept
{
  if (this->size_ > this->capacity_)
    return false;
  if ((this->size_ > 0) && (this->data_ == nullptr))
    return false;
  if ((this->capacity_ == 0) != (this->data_ == nullptr))
    return false;
  return true;
}

Observe algumas coisas sobre isso.

  • A função do predicado em si é conste noexcept, de acordo com a diretriz de que afirmações não devem ter efeitos colaterais. Se fizer sentido, também declare constexpr.
  • O predicado não afirma nada. Ele deve ser chamado de afirmações internas , como 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.
  • O fluxo de controle dentro da função é dividido em várias ifinstruções com returns 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.

auto numbers = std::vector<int> {};
numbers.push_back(14);
numbers.push_back(92);
assert(numbers.size() == 2);  // silly
assert(!numbers.empty());     // silly and redundant

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::vectorfunciona 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.

auto w = widget {};
w.enable_quantum_mode();
assert(w.quantum_mode_enabled());  // probably silly

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.

auto w = widget {};
// After reading the source code, I have concluded that quantum mode is
// always off by default but this isn't documented anywhere.
assert(!w.quantum_mode_enabled());

É melhor que a solução a seguir, que não informa se suas suposições estavam corretas.

auto w = widget {};
if (w.quantum_mode_enabled())
  {
    // I don't think that quantum mode is ever enabled by default but
    // I'm not sure.
    w.disable_quantum_mode();
  }

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…

if (!server_reachable())
  {
    log_message("server not reachable");
    shutdown();
  }

…ao invés disso.

assert(server_reachable());

Também nunca use asserções para validar entrada não confiável ou verificar std::mallocse returnvocê não nullptr. 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, como throwuma 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 para assert, 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.


4
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.
Blrfl

4

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.


4
Não entendi seu ponto de vista quando você diz “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. ”Para que você está usando afirmações, se não para verificar suas suposições?
precisa saber é o seguinte

4

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).


3

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:

  • garantir que nenhum efeito colateral esteja ocorrendo na declaração (fornecida por uma construção, conforme explicado acima)
  • desempenho durante testes de desenvolvimento; isso pode ser resolvido adicionando níveis (como registro) à instalação de afirmação; para que você possa desativar algumas declarações de uma construção de desenvolvimento para melhorar o desempenho

2
Caramba, você gosta da palavra "certo" e suas derivações. Eu conto 8 usos.
Casey Patton

sim, desculpe, eu tendem a camarilha de palavras demais - fixas, graças
lurscher

2

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.


Se o seu código falhar 15 afirmações por minuto, acho que há um problema maior envolvido. As asserções nunca devem ser acionadas em código livre de bugs e, caso contrário, devem matar o aplicativo para evitar mais danos ou colocá-lo em um depurador para ver o que está acontecendo.
precisa saber é o seguinte

2

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 simples assert.

Mensagem de falha

A única coisa em que consigo pensar é incorporar informações de falha ao assertse você ainda não o estiver fazendo, assim:

assert(n >= 0 && n < num && "Index is out of bounds.");

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, assertpode realmente ser mal utilizado e introduzir erros, como:

assert(foo() && "Call to foo failed!");

... 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:

vec3f cross_product(const vec3f& lhs, const vec3f& rhs)
{
    return vec3f
    (
        lhs[1] * rhs[2] - lhs[2] * rhs[1],
        lhs[2] * rhs[0] - lhs[0] * rhs[2],
        lhs[0] * rhs[1] - lhs[1] * rhs[0]
    );
}

... 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ão operator[]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).


1
Bem, pode haver "demasiadas" afirmações na teoria, embora esse problema se torne óbvio muito rápido: se a afirmação demorar consideravelmente mais do que a base da função. É certo que não me lembro de ter descoberto que, ainda na natureza, o problema oposto é predominante.
Deduplicator

@Duplicador Ah sim, eu encontrei esse caso nessas rotinas matemáticas críticas de vetores. Embora definitivamente pareça muito melhor errar do que muitos!

-1

É 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.


-3

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.


3
este não parece oferecer nada substancial sobre pontos feitos e explicado em anteriores 8 respostas
mosquito
Ao utilizar nosso site, você reconhece que leu e compreendeu nossa Política de Cookies e nossa Política de Privacidade.
Licensed under cc by-sa 3.0 with attribution required.