O que é uma asserção?
Uma asserção é uma condição que se deve sempre verificar-se num determinado ponto do programa.
Asserções em C
#include <assert.h>
assert( int cond );
- Se
cond
for diferente de 0: não faz nada
- Se
cond
for 0: imprime uma mensagem de erro e termina imediatamente a execução do programa
prog: some_file.c:16: some_func: Assertion ... failed.
Exemplo
#include <assert.h>
int main(void) {
int x = 5, y = -3;
assert( 1 + 1 == 2 ); // OK
assert( x*y > 0 ); // Assertion failed
printf("%d\n", x*y); // não executa
}
Porquê usar asserções
- Exprimir condições de funcionamento correto do programa
- Detetar erros de programação mais cedo
- Documentar propriedades do programa
- pré-condições à entrada de funções
- pós-condições à saida de funções
- invariantes de ciclos
Pré-condição
- Uma condição que se deve verificar à entrada de uma função
- Responsabilidade de quem chama a função
- A função só tem de retornar um resultado correto quando a pré-condição é verdadeira
- Ajudam a detetar erros na invocação da função
Exemplo
- Calcular o fatorial de um inteiro \(n\)
- Vamos usar uma asserção para a seguinte pré-condição:
- \(n\) deve ser maior ou igual a zero
Fatorial
#include <assert.h>
int factorial(int n) {
assert(n >= 0); // pré-condição
int r = 1;
for(int i = 2; i<=n; i++)
r = r*i;
return r;
}
int main(void) {
...
}
Execução
$ gcc -o fact fact.c
$ ./fact
Introduza um inteiro positivo:10
Factorial 10 = 362880
$ ./fact
Introduza um inteiro positivo:-1
fact: fact.c:6: factorial: Assertion `n >= 0' failed.
Observação
factorial
podia retornar qualquer resultado para n
negativo!
- a responsabilidade de garantir a pre-condição é de quem invoca a função
- O
assert
detecta o erro em vez de imprimir um resultado errado
Uso de pré-condições
Exprimir condições de entrada de uma função, e.g.:
- tamanhos e índices são válido;
- apontadores não são
NULL
.
int some_fun( char *ptr, int size )
{
assert( size <= LIMIT );
assert( ptr != NULL );
...
}
Pós-condição
- Uma condição que se deve verificar à saida de uma função
- Responsabilidade de quem implementa a função
- Quem invoca a função espera que seja verdadeira (não tem de verificar)
- Ajudam a detetar erros na implementação da função
Exemplo: Procurar um carater
int procurar(char str[], char ch);
- Procura a primeira ocorrência do charater
ch
na cadeia
- Retorna um índice ou -1 (se o carater não ocorre)
- O resultado \(i\) verifica a condição \[ i = -1 \lor (i \geq 0 \land i < \texttt{strlen}(\texttt{str})) \]
Asserção de pós-condições
int procurar(char str[], char ch) {
int i = 0;
while (str[i] != ch) {
if(str[i] == '\0') {
i = -1;
break; // não encontrou
}
i++;
}
assert(i == -1 || (i>=0 && i<strlen(str)));
return i;
}
Como usar pós-condições
- Se a função tiver multiplos pontos de saida devemos repetir a pós-condição
- Por vezes é preferíval re-escrever a função para ter um único
return
Invariante
- Uma condição que se verifica em cada iteração de um ciclo
- Exemplo: relações entre índices e tamanhos de variáveis indexadas
Exemplo: eliminar repetidos
int elimrep(int v[], int n);
- Eliminar ocorrências repetidas em
v
- Ficam apenas as primeiras ocorrências
- Ciclo com duas variáveis \(i\), \(k\) \[
\scriptsize
\overbrace{\begin{array}{|c|c|c|} \hline
\texttt{v[0]} & \cdots & \texttt{v[k-1]} \\ \hline
\end{array}}^{\text{sem repetidos}}
\overbrace{\begin{array}{|c|c|c|c|c|} \hline
\texttt{v[k]} & \cdots &\texttt{v[i]}&\cdots & \texttt{v[n-1]} \\ \hline
\end{array}}^{\text{?}}
\]
- Invariantes: \[ \begin{gather*}
0 \leq k \leq i \\ 0 \leq i < n \end{gather*} \]
Asserções de invariantes
int elimrep(int vec[], int n) {
int k = 0;
for(int i = 0; i < n; i++) {
assert(0 <= k && k <= i);
assert(0 <= i && i < n);
int val = vec[i];
if(!ocorre(vec, k, val)) {
vec[k++] = val;
// valor não repetido
}
}
return k;
}
Uso de invariantes
- Facilita a deteção de erros de índices
Assert failed
em vez de Segmentation Fault
- Obriga a pensar nos casos limite:
- o que acontence quando todos valores são repetidos?
- o que acontence quando todos valores são diferentes?
Asserções e erros
Distinguir entre:
- Erro de programação
uma situação que não deve ocorrer; por exemplo: usar um índice inválido ou dividir por zero.
- Erro de execução
uma situação que pode ocorrer e que devemos tratar; por exemplo: um input incompleto do utilizador.
Asserções são um mecanismo para detetar o primeiro tipo de erros — não para tratar os segundos.