Funzioni di Confronto per i Numeri in Virgola Mobile in Linguaggio C

Concetti Chiave
  • I numeri in virgola mobile devono essere confrontati utilizzando l'errore relativo per evitare risultati inaspettati dovuti a errori di arrotondamento e precisione limitata.
  • La libreria standard del linguaggio C fornisce macro specifiche per confrontare i numeri in virgola mobile in modo sicuro, tenendo conto di casi speciali come NaN e infiniti.
  • Le funzioni fmax, fmin e fdim consentono di calcolare rispettivamente il massimo, il minimo e la differenza positiva tra due numeri in virgola mobile, gestendo correttamente i casi in cui uno degli argomenti è NaN.

Il problema di confrontare i numeri in virgola mobile

Quando si lavora con i numeri interi, il confronto tra due valori è un'operazione semplice e diretta. Basta adoperare gli operatori relazionali standard come ==, !=, <, >, <= e >=.

Sebbene tali operatori funzionino anche con i numeri in virgola mobile (tipi float, double e long double), il confronto diretto tra questi tipi di dati può portare a risultati inaspettati a causa della natura approssimativa della rappresentazione dei numeri in virgola mobile.

Ad esempio, a causa di errori di arrotondamento e precisione limitata, due numeri che dovrebbero essere uguali potrebbero non esserlo esattamente quando rappresentati in virgola mobile. Questo può causare problemi quando si scrive un codice di questo tipo:

double x;
double y;

// Supponiamo che x e y vengano calcolati in qualche modo

if (x == y) {
    // Fai qualcosa se x è uguale a y
} else {
    // Fai qualcos'altro se x non è uguale a y
}

Il problema, in questo codice, è che x e y potrebbero non essere esattamente uguali a causa di piccole differenze dovute alla rappresentazione in virgola mobile, anche se matematicamente dovrebbero esserlo. Quindi il confronto di uguaglianza x == y potrebbe restituire false anche quando ci si aspetta che sia true.

Per questo motivo, il modo corretto di confrontare i numeri in virgola mobile è utilizzare l'errore relativo.

Matematicamente, l'errore relativo tra due numeri a e b è definito come:

\text{err}_r = \frac{|a - b|}{|b|}

Questo valore rappresenta il rapporto tra l'errore assoluto (ossia la differenza assoluta tra a e b, |a - b|) e il valore di riferimento b. In soldoni, ci dice in percentuale quanto a si discosta da b.

Per confrontare due numeri in virgola mobile x e y, si può calcolare l'errore relativo tra di essi e verificare se questo errore è inferiore a una soglia di tolleranza predefinita, spesso chiamata "epsilon". Se l'errore relativo è inferiore a questa soglia, si considera che i due numeri siano "praticamente uguali". Quindi il codice di sopra può essere riscritto in questo modo:

double x;
double y;

double epsilon = 1e-10; // Soglia di tolleranza

if (fabs(x - y) / fabs(y) < epsilon) {
    // Fai qualcosa se x è praticamente uguale a y
} else {
    // Fai qualcos'altro se x non è praticamente uguale a y
}

Per rendersi conto di questo problema, si può considerare il seguente esempio:

#include <stdio.h>

int main() {
    double a = 0.1 + 0.2;
    double b = 0.3;

    if (a == b) {
        printf("a e b sono uguali\n");
    } else {
        printf("a e b non sono uguali\n");
    }

    return 0;
}

In questo esempio, stiamo effettuando la somma:

a = 0.1 + 0.2

dopodiché, confrontiamo a con b, che è uguale a 0.3. Nonostante matematicamente a e b dovrebbero essere uguali, il confronto diretto a == b restituirà false a causa della rappresentazione approssimativa dei numeri in virgola mobile.

Infatti, se si esegue questo codice, l'output sarà:

a e b non sono uguali

La motivazione di questo comportamento risiede nel fatto che 0.1, 0.2 e 0.3 non possono essere rappresentati esattamente in binario, portando a piccole imprecisioni nei calcoli.

Quindi, per confrontare correttamente a e b, si dovrebbe utilizzare l'errore relativo come descritto in precedenza:

#include <stdio.h>
#include <math.h>

int main() {
    double a = 0.1 + 0.2;
    double b = 0.3;
    double epsilon = 1e-10; // Soglia di tolleranza

    if (fabs(a - b) / fabs(b) < epsilon) {
        printf("a e b sono praticamente uguali\n");
    } else {
        printf("a e b non sono praticamente uguali\n");
    }

    return 0;
}
Nota

I numeri in virgola mobile devono sempre essere confrontati utilizzando l'errore relativo

Poiché i numeri in virgola mobile possono introdurre errori di arrotondamento e precisione limitata, è fondamentale confrontarli utilizzando l'errore relativo piuttosto che gli operatori relazionali standard. Questo approccio aiuta a evitare risultati inaspettati e garantisce che i confronti siano più affidabili:

\frac{|a - b|}{|b|} &lt; \epsilon

dove \epsilon è una soglia di tolleranza predefinita.

Fortunatamente, la libreria standard del linguaggio C fornisce delle funzioni specifiche per confrontare i numeri in virgola mobile in modo sicuro e affidabile, tenendo conto delle peculiarità di questi tipi di dati. Queste funzioni sono definite nell'header <math.h>.

Vediamole in dettaglio.

Macro di confronto per i numeri in virgola mobile

La libreria standard del linguaggio C definisce diverse macro per confrontare i numeri in virgola mobile in modo sicuro. Queste macro sono progettate per gestire le peculiarità dei numeri in virgola mobile, come NaN (Not a Number) e infiniti.

Tali macro parametriche sono:

Macro Descrizione
int isgreater(real x, real y); Restituisce un valore diverso da zero se x è maggiore di y.
int isgreaterequal(real x, real y); Restituisce un valore diverso da zero se x è maggiore o uguale a y.
int isless(real x, real y); Restituisce un valore diverso da zero se x è minore di y.
int islessequal(real x, real y); Restituisce un valore diverso da zero se x è minore o uguale a y.
int islessgreater(real x, real y); Restituisce un valore diverso da zero se x è minore o maggiore di y (ossia, se x e y non sono uguali).
int isunordered(real x, real y); Restituisce un valore diverso da zero se almeno uno tra x e y è NaN (Not a Number).
Tabella 1: Macro per la comparazione di numeri in virgola mobile in linguaggio C

La prima osservazione da fare riguarda il fatto che tali macro accettano come argomenti dei valori di qualunque tipo in virgola mobile: float, double o long double. Il tipo specifico viene dedotto automaticamente dal compilatore in base ai tipi degli argomenti passati. Il tipo di ritorno è sempre int ma il valore restituito è 0 (falso) o diverso da 0 (vero), a seconda del risultato del confronto.

Le macro isgreater, isgreaterequal, isless e islessequal svolgono, sostanzialmente, le stesse operazioni degli operatori relazionali >, >=, < e <=, ma in modo sicuro per i numeri in virgola mobile. Esse tengono conto di casi speciali come NaN e infiniti, garantendo risultati affidabili.

La macro islessgreater è utile per verificare se due numeri in virgola mobile sono diversi tra loro e ha due peculiarità:

  1. Gestisce correttamente i casi in cui uno o entrambi gli argomenti sono NaN, restituendo 0 (falso) in tali situazioni.
  2. Utilizza come epsilon una soglia di tolleranza predefinita: DBL_EPSILON, FLT_EPSILON o LDBL_EPSILON, a seconda del tipo di dato in virgola mobile utilizzato.

L'ultima macro, isunordered, è particolarmente utile per verificare se almeno uno dei due numeri in virgola mobile è NaN. Questo è importante perché qualsiasi confronto con NaN restituisce sempre false, quindi questa macro consente di gestire correttamente tali situazioni.

Funzioni di Minimo, Massimo e Differenza Positiva

Oltre alle macro di confronto, la libreria standard del linguaggio C fornisce anche delle funzioni per calcolare il minimo, il massimo e la differenza positiva tra due numeri in virgola mobile. Queste funzioni sono anch'esse definite nell'header <math.h>.

Le funzioni sono:

Funzione Descrizione
double fmax(double x, double y); Restituisce il massimo tra x e y. Se uno dei due è NaN, restituisce l'altro valore.
float fmaxf(float x, float y); versione per float di fmax.
long double fmaxl(long double x, long double y); versione per long double di fmax.
double fmin(double x, double y); Restituisce il minimo tra x e y. Se uno dei due è NaN, restituisce l'altro valore.
float fminf(float x, float y); versione per float di fmin.
long double fminl(long double x, long double y); versione per long double di fmin.
double fdim(double x, double y); Restituisce la differenza positiva tra x e y, ossia x - y se x > y, altrimenti 0. Se uno dei due è NaN, restituisce NaN.
float fdimf(float x, float y); versione per float di fdim.
long double fdiml(long double x, long double y); versione per long double di fdim.
Tabella 2: Funzioni di Minimo, Massimo e Differenza Positiva per i numeri in virgola mobile in linguaggio C

Alcune osservazioni su queste funzioni:

  • Ogni funzione ha tre versioni, una per ciascun tipo di dato in virgola mobile: float, double e long double. Il suffisso f indica la versione per float, mentre il suffisso l indica la versione per long double.
  • Le funzioni fmax e fmin gestiscono correttamente i casi in cui uno degli argomenti è NaN, restituendo l'altro valore. Questo è utile per evitare risultati imprevisti quando si lavora con numeri in virgola mobile che potrebbero essere NaN.
  • La funzione fdim calcola la differenza positiva tra due numeri in virgola mobile, restituendo 0 se il primo numero è minore o uguale al secondo. Anche in questo caso, se uno degli argomenti è NaN, la funzione restituisce NaN. Matematicamente, fdim(x, y) può essere espressa come:

    \text{fdim}(x, y) = \begin{cases} x - y &amp; \text{se } x &gt; y \\ +0 &amp; \text{altrimenti} \end{cases}