CodeGym /Corsi /C++ SELF /Politica sugli avvisi: «warnings as errors»

Politica sugli avvisi: «warnings as errors»

C++ SELF
Livello 30, Lezione 3
Disponibile

1. Perché serve una politica sugli avvisi

Se si guardano gli avvisi del compilatore come a delle sbavature, è vero che infastidiscono: il codice compila, il programma produce qualcosa — che altro si vuole? Ma se si considera il warning come un avviso precoce di un guasto, diventa un alleato. Il compilatore non si stanca, non dimentica, non «più o meno ricorda cosa voleva l'autore», e quindi mostra onestamente i punti in cui il vostro codice è ambiguo o rischioso.

Immaginate un semaforo: un errore di compilazione è rosso, non si procede. Un warning è giallo. Formalmente si può andare, ma se ignorate sempre il giallo, vi capiterà presto di conoscere il cosiddetto «riparare il paraurti a proprie spese». La politica sugli avvisi è un accordo nel progetto su come trattare questo «giallo»: rallentare e controllare, oppure «premere sull'acceleratore e sperare».

A proposito, è curioso che anche nei grandi documenti tecnici gli sviluppatori spendano tempo a «sistemare i warnings» — lì possono essere avvisi di impaginazione come “overfull hbox warnings” durante la build della documentazione. È un buon ancoraggio psicologico: gli avvisi non sono una cosa trascurabile, ma un indicatore di qualità che è bene tenere sotto controllo.

Cosa significa in pratica «warnings as errors»

«Warnings as errors» significa una cosa semplice: il progetto concorda che un avviso del compilatore è considerato un fallimento della build, quasi come un errore. Quindi se il compilatore segnala qualcosa di «sospetto», la build si ferma finché non chiarite l'intenzione del codice. Non perché il compilatore sia «permale», ma perché il progetto risparmia tempo al vostro io futuro (e a quelli dei colleghi), evitando che i punti ambiguhi si accumulino.

Per non confondere i concetti, è utile tenere a mente questa tabella:

Segnale dal compilatore Il programma si compilerà? Cosa significa di solito Come reagire
Error No Il codice è sicuramente invalido Correggere obbligatoriamente
Warning Sì Il codice è valido, ma l'intento/la sicurezza sono dubbi Capire e rendere l'intento esplicito
Nothing Sì Secondo il compilatore tutto sembra sensato Perfetto, si prosegue

È importante capire: la politica «warnings as errors» non dice che ogni warning = bug. Dice qualcos'altro: «ogni warning = un punto dove prendere una decisione consapevole». Quindi o correggete un reale problema, oppure mostrate esplicitamente che la scelta è intenzionale e sicura.

Principio fondamentale: rendere chiaro l'intento del codice

L'errore più comune dei principianti è cercare di «rimuovere il warning a qualsiasi costo». In C++ questo è particolarmente pericoloso, perché «a qualsiasi costo» spesso si traduce in uno static_cast messo ovunque. Il codice compila — sì. Ma il significato? A volte avete semplicemente tappato una spia «Check engine» con del nastro adesivo.

L'idea corretta: il compilatore avvisa dove il vostro intento non è evidente. Quindi dovete o chiarire l'intento con il tipo giusto, o con la struttura del codice, o con controlli sui limiti, o con un cast consapevole (in punti rari).

Alleneremo questo approccio su situazioni brevi che avete già incontrato nel corso: valori non usati, size() e indici, conversioni implicite di tipo. E sì, sarà come fare ordine nella stanza: all'inizio sgradevole, ma poi improvvisamente ritrovate il caricabatterie del telefono che credevate disperso dal 2019.

2. Le tre cause più frequenti di warnings tra i principianti e soluzioni sensate

Variabile non usata: i «resi» dopo le modifiche

La variabile non usata è uno dei warnings più utili per i principianti. Spesso indica: avete dimenticato di completare la logica, o avete lasciato spazzatura dopo un esperimento, o avete sbagliato nome di variabile. Queste cose sono fastidiose perché creano l'illusione: «l'ho calcolata da qualche parte, quindi viene usata».

Guardate il mini-esempio:

#include <iostream>

int main() {
    int answer = 42;               // warning: unused variable 'answer'
    std::cout << "Hello!\n";       // Hello!
}

Se la variabile non serve davvero, la soluzione più corretta è cancellarla. Ma ci sono casi intermedi: scrivete il codice «a tappe», la variabile servirà fra cinque minuti, ma ora la politica “warnings as errors” infastidisce. In quel caso potete dichiarare esplicitamente al compilatore: «sì, so che non la uso». Il trucco più semplice (senza attributi e senza disabilitare gli avvisi globalmente) è castare a void:

#include <iostream>

int main() {
    int answer = 42;
    static_cast<void>(answer);     // esplicito: al momento non la usiamo
    std::cout << "Hello!\n";       // Hello!
}

Non è «magia» né un «espediente per ingannare il sistema»: è una piccola annotazione «qui è intenzionale». Ma se queste annotazioni aumentano troppo, di solito è indice che il codice richiede refactoring o che state mantenendo troppi bozzetti nel ramo principale.

Signed/unsigned nei cicli sui contenitori

Un vettore restituisce la dimensione con il tipo std::size_t (tipo unsigned). Un principiante scrive l'indice come int, perché «l'indice è un numero». Nel confronto i < v.size() il compilatore può avvertire: si confrontano signed e unsigned, possibili sorprese ai bordi.

Mini‑esempio:

#include <iostream>
#include <vector>

int main() {
    std::vector<int> v{10, 20, 30};

    for (int i = 0; i < v.size(); ++i) {     // warning: signed/unsigned compare
        std::cout << v[i] << '\n';
    }
}

La correzione semantica giusta: se i è un indice del contenitore, anche il tipo dovrebbe essere «da contenitore». La soluzione più semplice è usare std::size_t:

#include <cstddef>
#include <iostream>
#include <vector>

int main() {
    std::vector<int> v{10, 20, 30};

    for (std::size_t i = 0; i < v.size(); ++i) {
        std::cout << v[i] << '\n';
    }
}

Notate: non abbiamo «sconfitto il warning», abbiamo reso il codice più logico. Ora dal tipo si capisce che i è un indice e il confronto è coerente.

Un dettaglio importante: a volte volete scorrere a ritroso gli indici (per esempio da size() - 1 fino a 0). Qui std::size_t può essere una trappola perché è unsigned e l'espressione «i >= 0» è sempre vera. In quei casi è meglio cambiare la struttura del ciclo (per esempio usare un altro pattern di iterazione). Ora il nostro obiettivo è capire il principio «il tipo esprime l'intento», non imparare 12 forme di cicli.

Conversioni implicite e perdita di dati

La terza «fucina di warnings» sono le conversioni implicite, specialmente quando può esserci perdita di informazione. Il classico: il prezzo è un double, ma volete i centesimi in int. Il compilatore sospetta che abbiate dimenticato l'arrotondamento e avverte.

#include <iostream>

int main() {
    double price = 3.99;
    int euros = price;            // warning: implicit conversion, loss of data
    std::cout << euros << '\n';   // 3
}

Se volete davvero scartare la parte frazionaria (non la miglior pratica finanziaria, ma come esempio didattico va bene), fatelo in modo esplicito:

#include <iostream>

int main() {
    double price = 3.99;
    int euros = static_cast<int>(price);    // perdita frazionaria intenzionale
    std::cout << euros << '\n';             // 3
}

Pensiero importante: static_cast non è una «panacea universale per i warnings». È un marcatore: «so cosa sto facendo». Se usate static_cast senza capire, state semplicemente imparando a scrivere codice più pericoloso con mano più sicura.

Come introdurre «warnings as errors» senza rovinarsi la vita

Se attivate «warnings as errors» in un progetto dove già ci sono decine di warning, la sensazione sarà quella di una pulizia generale in cui aprite l'armadio e trovate scatoloni dell'epoca dei dinosauri. Formalmente state facendo qualcosa di buono, ma psicologicamente la tentazione è spegnere tutto e andare a fare altro.

La strategia pratica è: prima portare la build a uno stato «silenzioso» senza warning con le impostazioni correnti, e solo dopo rendere gli avvisi degli errori. Non è una questione di debolezza, ma di riduzione del rumore: se ci sono molti warning, smettete di notarli. Se gli warning sono zero, ogni nuovo warning è un evento e lo noterete subito.

Un'altra buona abitudine: correggere un warning in modo che il codice diventi più leggibile, non più complesso. Se dopo aver sistemato un warning vi ritrovate con tre cast, due costanti misteriose e un commento «non toccare, funziona» — probabilmente avete curato il sintomo, non la causa.

5. MiniTracker e gli avvisi come controllo di qualità

Immaginiamo che a questo punto del corso abbiamo una piccola applicazione console chiamata MiniTracker, che mantiene semplici «task» in memoria e li stampa. Ora non ci interessa la bellezza architetturale — ci interessa che gli esempi siano vivi e mantengano un contesto coerente.

Iniziamo con un template molto semplice per «stampare la lista di task», dove è facile incappare in avvisi.

#include <iostream>
#include <string>
#include <vector>

int main() {
    std::vector<std::string> tasks{"Read", "Code", "Sleep"};

    for (int i = 0; i < tasks.size(); ++i) {     // potenziale warning
        std::cout << i << ": " << tasks[i] << '\n';
    }
}

Se nel progetto è attiva una politica severa sugli avvisi, questo ciclo potrebbe far fallire la build. Lo correggiamo non perché «il compilatore faccia il difficile», ma perché il ciclo è scritto meno precisamente di quanto potrebbe essere.

#include <cstddef>
#include <iostream>
#include <string>
#include <vector>

int main() {
    std::vector<std::string> tasks{"Read", "Code", "Sleep"};

    for (std::size_t i = 0; i < tasks.size(); ++i) {
        std::cout << i << ": " << tasks[i] << '\n';
    }
}

Ora aggiungiamo a MiniTracker una funzione «bozza» che per il momento non usiamo. Succede spesso: buttate giù l'API ma non l'avete ancora collegata.

#include <string>

int countDone(const std::string& task) {
    // TODO: più tardi decidiamo cosa significa "done"
    return 0;                       // warning: task is unused (potenzialmente)
}

Se la build fallisce a causa dell'avviso sul parametro non usato, avete una scelta onesta: rimuovere il parametro se non serve, oppure mostrare esplicitamente «al momento non usato, ma il contratto è questo». Per il contesto odierno (senza entrare negli attributi) potete scrivere così:

#include <string>

int countDone(const std::string& task) {
    static_cast<void>(task);        // sì, per ora non lo usiamo
    return 0;
}

Questo è più pulito che disabilitare gli avvisi globalmente, perché spegnere gli avvisi è come disattivare un allarme antincendio perché suona. È silenzioso, ma più sicuro? Non necessariamente.

Mini-schema: gli avvisi come «filtro» di qualità nella build

Per consolidare l'idea, è utile rappresentare «warnings as errors» come una piccola barriera nella pipeline di build. Il programma passa ancora attraverso preprocessing → compilazione → link, ma tra compilazione e successo appare una condizione aggiuntiva: «non devono esserci warning».

flowchart TD
    A[File sorgenti .cpp/.hpp] --> B[Preprocessing]
    B --> C[Compile: file oggetto]
    C --> D{Ci sono warning?}
    D -->|Sì| E[Build fallita
si corregge il codice] D -->|No| F[Link: eseguibile]

Dal punto di vista psicologico è una cosa potente: smettete di «abituarvi alle spie gialle». Appena compare un nuovo warning, lo prendete subito — proprio mentre ricordate cosa avete modificato.

6. Errori tipici quando si introduce «warnings as errors»

Errore n.1: attivare la politica severa in un progetto che già ha molti warning e poi soffrire eroicamente.
Quando gli avvisi sono molti, attivare «warnings as errors» trasforma lo sviluppo in «tutto è rotto». Si comincia a correggere senza senso, solo per far compilare, e il codice facilmente diventa una collezione di cast strani. È molto più sano prima portare il progetto a «warnings = 0» e poi irrigidire le regole.

Errore n.2: curare gli avvisi disabilitando gli avvisi.
Disabilitare un warning è più semplice che capirne la causa. Ma spegnendo una diagnosi «in largo», si perde un segnale importante non solo lì, ma anche in futuri punti del codice. È come incollare il check-engine con lo scotch: la spia non dà più fastidio, ma il motore non è migliorato.

Errore n.3: trasformare static_cast in una pillola universale.
Il cast esplicito è uno strumento che dovrebbe venire accompagnato dalla risposta interna «perché è sicuro?». Se non c'è una risposta, non state risolvendo il problema — lo state nascondendo. Particolarmente pericoloso è convertire unsigned in signed (o viceversa) «per far sparire il warning» senza considerare limiti e overflow.

Errore n.4: correggere il warning in modo che l'intento diventi meno chiaro.
A volte si può eliminare un warning in tre modi diversi. Quello giusto è di solito il modo che rende il codice più trasparente. Se la correzione ha peggiorato la leggibilità, probabilmente avete scelto la strada sbagliata, o il problema è più profondo (per esempio la funzione non ha un contratto chiaro o il tipo è inappropriato).

Errore n.5: ignorare gli avvisi in «piccoli punti» perché «lì è ovvio».
Il compilatore non sa cosa per voi è «ovvio». Vede solo codice. Spesso l'avviso appare dove siete convinti che tutto sia a posto… finché non cambiano i dati in input, la dimensione del contenitore o il tipo della variabile. La disciplina «warnings as errors» serve proprio a non lasciare questi punti «al caso».

1
Compito
C++ SELF, livello 30, lezione 3
Bloccato
Compilazione pulita
Compilazione pulita
1
Compito
C++ SELF, livello 30, lezione 3
Bloccato
Parametro per il futuro
Parametro per il futuro
1
Compito
C++ SELF, livello 30, lezione 3
Bloccato
Indici sicuri
Indici sicuri
1
Compito
C++ SELF, livello 30, lezione 3
Bloccato
Cent senza perdite
Cent senza perdite
Commenti
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION