CodeGym /Corsi /C++ SELF /Flag del compilatore: -std=c++23, -O0/ -O2, -g e avvis...

Flag del compilatore: -std=c++23, -O0/ -O2, -g e avvisi

C++ SELF
Livello 33, Lezione 3
Disponibile

1. I flag sono parte del contratto di build

Quando si inizia a compilare un progetto dalla console, viene davvero voglia che il compilatore sia come un microonde: premi un pulsante e ottieni il risultato. Ma il compilatore somiglia piuttosto a uno chef molto pignolo: la stessa «ricetta» (il vostro codice) può riuscire in modi diversi a seconda della modalità, della temperatura e del fatto che gli abbiate permesso oppure no di lamentarsi degli ingredienti sospetti.

I flag non sono «opzioni aggiuntive per perfezionisti». Sono un modo per fissare le regole della build in modo che il codice si comporti in maniera prevedibile per voi e per chiunque altro: stesso standard del linguaggio, livello di ottimizzazione chiaro, presenza di informazioni diagnostiche e avvisi. Se non si fissano i flag, si finisce facilmente nella situazione «da me compila», «da te invece no», e avrete ragione entrambi (il che è particolarmente frustrante).

È comodo pensare alla build così:

flowchart LR
    A[Sorgenti .cpp/.hpp] --> B[Compilazione: g++/clang++ + flag]
    B --> C[File oggetto .o/.obj]
    C --> D[Linking]
    D --> E[Eseguibile]

I flag influiscono soprattutto sul blocco di compilazione (cosa considerare errore o avviso, quali regole del linguaggio sono attive, cosa ottimizzare) e in parte anche su ciò che si «attacca» al risultato finale (per esempio i simboli per il debug).

2. -std=c++23: fissiamo il «linguaggio» con cui si parla al compilatore

Nella vita quotidiana non pensiamo a quale lingua stiamo parlando: semplicemente parliamo. Il compilatore, purtroppo, non è così telepatico: senza un suggerimento può usare uno standard «predefinito», che dipende dalla versione del compilatore e dalle impostazioni dell’ambiente. Oggi potrebbe essere C++17, domani C++20, e dopodomani aprirete un vecchio progetto su un’altra macchina: a quel punto inizierà la serie «perché non si compila».

Il flag -std=c++23 è il vostro accordo scritto con il compilatore: «giochiamo secondo le regole di C++23». Anche se per ora non state usando le feature «più nuove», la disciplina conta: rende la build riproducibile.

Comando tipico:

g++ -std=c++23 main.cpp -o app
# oppure
clang++ -std=c++23 main.cpp -o app

Mini-diagnostica tramite __cplusplus

Per verificare di essere davvero nella modalità giusta, si può stampare il valore della macro predefinita __cplusplus. È proprio un «indicatore della modalità del linguaggio», non un «numero di versione del compilatore». La macro stessa fa parte dell’insieme standard delle entità predefinite.

#include <iostream>

int main() {
    std::cout << "__cplusplus = " << __cplusplus << '\n';
    // Per esempio: __cplusplus = 202302 (il valore dipende dal compilatore)
}

Se compilate senza -std=c++23, il valore può essere diverso. E sì, a volte i compilatori si comportano in modo differente, ma l’idea stessa del controllo rimane utile: almeno vedete che la modalità è davvero cambiata.

3. Avvisi: -Wall -Wextra -Wpedantic — «compilatore, brontola più forte»

Mentre si impara, il compilatore è il vostro reviewer gratuito. Non si stanca, non chiede un aumento e non scrive nel PR «qui è tutto sbagliato» senza dettagli (di solito). Ma di default è piuttosto educato: alcuni punti dubbi li lascia passare in silenzio. I flag degli avvisi abilitano la modalità «sii onesto, anche se non mi farà piacere».

Set di base che nei progetti didattici conviene quasi sempre abilitare:

  • -Wall — abilita molti avvisi comuni (storicamente il nome è ingannevole: non significa «all warnings», ma resta comunque molto utile).
  • -Wextra — aggiunge un’altra porzione di avvisi, spesso su cose del tipo «più o meno funziona, ma ha un’aria sospetta».
  • -Wpedantic — abilita avvisi su estensioni non standard e su punti «scivolosi» dal punto di vista dello standard.

Il comando è questo:

g++ -std=c++23 -Wall -Wextra -Wpedantic main.cpp -o app

Esempio 1: «la variabile c’è, il senso no» (unused variable)

Partiamo da una situazione semplice: avete scritto una variabile, poi avete cambiato idea, ma vi siete dimenticati di cancellarla. Per una persona è una sciocchezza, per un progetto fra un mese è spazzatura che rende il codice più difficile da leggere e a volte nasconde errori reali.

#include <iostream>

int main() {
    int debugValue = 42;           // warning: la variabile non è usata
    std::cout << "Hello!\n";       // Hello!
}

Con gli avvisi attivi, il compilatore vi dirà con garbo: «mi sa che ti sei dimenticato qualcosa». Ed è davvero più utile di quanto sembri: spesso è così che si scoprono rami logici «lasciati a metà».

Esempio 2: confronto signed/unsigned (un classico)

Questo problema è già noto dai temi su size_t: v.size() restituisce un tipo senza segno, mentre int è con segno. Se li confrontate direttamente, il compilatore può avvisarvi. E ha ragione: in alcuni casi limite la logica può diventare inattesa.

#include <vector>

int main() {
    std::vector<int> v{1, 2, 3};

    for (int i = 0; i < v.size(); ++i) { // spesso warning: confronto signed/unsigned
        (void)v[i];
    }
}

Qui non entriamo ancora nello stile ideale per iterare sui container (abbiamo altre lezioni per questo). Per ora è importante vedere l’idea: gli avvisi evidenziano i punti in cui «molto probabilmente va tutto bene», ma la probabilità di una sorpresa è più alta del normale.

4. -Werror: trasformiamo il «brontolio» in «stop, così non si può fare»

Dopo aver abilitato gli avvisi, nasce la tentazione di fare il passo successivo: «e se vietassimo la build quando ci sono avvisi?». È esattamente ciò che fa -Werror: ogni avviso diventa un errore di compilazione. Di conseguenza, la build o è perfettamente pulita oppure non passa.

Comando:

g++ -std=c++23 -Wall -Wextra -Wpedantic -Werror main.cpp -o app

Sembra un sogno. In pratica è davvero una disciplina potente, ma va attivata sapendo che state firmando per «il compilatore non ti lascerà più essere pigro». A volte è irritante: volevate controllare al volo un’idea, e lui pretende che togliate la variabile unused e sistemiate i tipi.

C’è un compromesso utile: in fase di studio si può compilare senza -Werror, ma attivarlo periodicamente «per controllare la pulizia» quando si è pronti a rimettere in ordine il codice. Nei team reali -Werror viene spesso abilitato nel CI, in modo che la codebase non degradi.

5. Ottimizzazione: -O0 e -O2 — «veloce» vs «chiaro cosa sta succedendo»

Quando un programma va lento, un principiante di solito pensa: «mi serve un computer più veloce». Uno sviluppatore esperto pensa prima: «per caso non l’ho compilato senza ottimizzazioni?». Il compilatore sa accelerare il codice, a volte in modo molto evidente, ma lo fa al prezzo di rendere il risultato meno «simile» al sorgente a livello di istruzioni macchina.

Il flag di ottimizzazione si imposta come -O...:

  • -O0 — ottimizzazioni disattivate (di solito è la modalità «per me contano di più una diagnostica prevedibile e la chiarezza»).
  • -O2 — tipico livello «forte» di ottimizzazione, spesso usato per una normale build veloce.

Mini-esempio in cui l’ottimizzazione ha davvero senso

Non misureremo i tempi di esecuzione (è un altro tema grande), ma prendiamo un codice in cui vengono eseguite molte operazioni.

#include <iostream>

long long sum_to(int n) {
    long long s = 0;
    for (int i = 1; i <= n; ++i) {
        s += i;
    }
    return s;
}

int main() {
    std::cout << sum_to(1'000'000) << '\n'; // 500000500000
}

Si può compilare così:

g++ -std=c++23 -O0 main.cpp -o app_debug
g++ -std=c++23 -O2 main.cpp -o app_fast

Il punto non è che -O2 «accelererà magicamente qualunque codice di mille volte». Il punto è un altro: si fissa una modalità. Se state testando la correttezza e volete che tutto sia il più possibile «trasparente», scegliete -O0. Se compilate «per farlo andare più veloce», scegliete -O2.

È importante ricordare che l’ottimizzazione può cambiare la «comodità nell’analizzare i problemi»: con -O2 il compilatore può riordinare i calcoli, eliminare variabili superflue e in generale fare in modo che il quadro di debug diventi meno evidente. Non è un «male»: è il prezzo della velocità.

6. -g: informazioni diagnostiche, così gli errori hanno «un indirizzo»

La situazione preferita del programmatore alle prime armi è: «è andato in crash, ma dove non si sa». Per far sì che il «dove» sia noto, il compilatore può aggiungere al risultato dati supplementari: il collegamento tra codice macchina e righe del sorgente, i nomi delle funzioni, delle variabili (in parte) e altre informazioni di servizio.

Il flag -g abilita la generazione di informazioni di debug/diagnostiche:

g++ -std=c++23 -g main.cpp -o app_dbg

Spesso -g viene usato insieme a -O0, così il debug è più «onesto»:

g++ -std=c++23 -O0 -g main.cpp -o app_dbg

Un dettaglio tecnico utile da tenere a mente senza fanatismo: -g aumenta soprattutto la dimensione degli artefatti e rende la diagnostica più ricca. Non è direttamente un «flag che rallenta il programma»; di solito sulla velocità incide molto di più proprio -O0 vs -O2.

E sì, useremo ancora questo flag quando arriveremo agli strumenti di debug. Oggi ci basta capire questo: -g fa in modo che un errore non sia soltanto «Segmentation fault (core dumped)» (in senso lato), ma abbia almeno la possibilità di diventare qualcosa di simile a «è andato in crash in una certa funzione, a una certa riga».

Profili di build: un set di flag per tutti i .cpp

Quando un progetto arriva ad avere anche solo due file .cpp, compare una nuova trappola: compilare un file con certi flag e un altro con flag diversi. Tecnicamente si può fare, ma in pratica il progetto si trasforma in una quest «indovina perché si comporta in modo strano».

La regola è semplice: i flag chiave (-std=..., warning, -O..., -g) devono essere applicati in modo uniforme a tutte le unità di traduzione.

Sotto ci sono due «profili» comodi da tenere come modelli.

Profilo A cosa serve Comando tipico
Build di debug diagnostica, verifica della logica
-std=c++23 -O0 -g -Wall -Wextra -Wpedantic
Build veloce esecuzione normale, velocità
-std=c++23 -O2 -Wall -Wextra -Wpedantic

In forma di comandi:

# Debug
g++ -std=c++23 -O0 -g -Wall -Wextra -Wpedantic main.cpp -o app_dbg

# Fast
g++ -std=c++23 -O2 -Wall -Wextra -Wpedantic main.cpp -o app_fast

7. Compilazione separata: gli stessi flag, ma con -c

Ora colleghiamo i flag a uno scenario reale «multi-file», così non sembrerà che questa magia valga solo per main.cpp. Immaginiamo il nostro progetto didattico (chiamiamolo convenzionalmente TaskList): un piccolo programma da console che stampa un elenco di attività. Il codice è volutamente semplice: qui conta più la build che la funzionalità.

File del progetto

Supponiamo di avere questi file:

  • include/task.hpp — dichiarazione delle funzioni.
  • src/task.cpp — definizioni delle funzioni.
  • src/main.cpp — punto di ingresso.

include/task.hpp:

#pragma once
#include <string>
#include <vector>

void print_tasks(const std::vector<std::string>& tasks);

src/task.cpp:

#include "task.hpp"
#include <iostream>

void print_tasks(const std::vector<std::string>& tasks) {
    for (const auto& t : tasks) {
        std::cout << "- " << t << '\n';
    }
}

src/main.cpp:

#include "task.hpp"
#include <vector>

int main() {
    std::vector<std::string> tasks{"Buy milk", "Learn C++ flags"};
    print_tasks(tasks);
}

Compilazione separata con un set unico di flag

Ora compiliamo ogni .cpp in un file oggetto, con gli stessi flag:

g++ -std=c++23 -O0 -g -Wall -Wextra -Wpedantic -Iinclude -c src/task.cpp -o task.o
g++ -std=c++23 -O0 -g -Wall -Wextra -Wpedantic -Iinclude -c src/main.cpp -o main.o

E il linking:

g++ main.o task.o -o tasklist_dbg

Fate attenzione a un dettaglio piccolo ma importante: -std=..., -O0, -g, gli avvisi, -Iinclude — tutto questo riguarda la compilazione dei .cpp in .o. Il linking, nel caso semplice, è già la fase «assembla ciò che è venuto fuori». Se dimenticate di applicare i flag a uno dei .cpp, «avrete due mondi diversi», e il compilatore non è affatto obbligato a consolarvi.

8. Combinazioni frequenti di flag e cosa significano davvero

Per non tenere tutto in testa come fossero formule magiche, è utile avere una mappa compatta.

Flag Cosa cambia Significato pratico
-std=c++23
regole del linguaggio e disponibilità di parte della libreria «compiliamo tutto con un unico standard, senza sorprese»
-Wall
set di avvisi «nota ciò che è evidentemente sospetto»
-Wextra
avvisi aggiuntivi «nota ciò che è meno evidente»
-Wpedantic
avvisi su elementi non standard «non abituarti alle estensioni come se fossero la norma»
-Werror
avvisi → errori «nel progetto non ci sono avvisi, punto»
-O0
ottimizzazione disattivata «diagnostica e comprensione più semplici, più lento»
-O2
ottimizzazione forte «di solito più veloce, ma il debug è meno trasparente»
-g
simboli di debug/collegamento al sorgente «errori e analisi più vicini alle righe di codice»

9. Errori tipici quando si lavora con i flag del compilatore

Errore n. 1: non indicare -std=... e sperare che «vada in qualche modo».
Spesso finisce così: il progetto compila per una persona e non compila per un’altra, anche se i sorgenti sono identici. La causa è banale: standard predefiniti diversi in versioni diverse del compilatore. La cura è ancora più banale, ma affidabile: scrivere sempre esplicitamente -std=c++23.

Errore n. 2: attivare gli avvisi una volta, vedere molti messaggi e… disattivarli per sempre.
È una reazione molto umana: il compilatore brontola, quindi «mi ostacola». In pratica, però, gli avvisi sono una diagnosi precoce di bug futuri. Di solito è meglio non spegnere il brontolio, ma mettere gradualmente in ordine il codice: togliere l’inutilizzato, correggere i tipi, rendere più chiare le intenzioni.

Errore n. 3: usare -Werror come randello all’inizio dell’apprendimento.
-Werror rende la build «più severa della vita reale», e per un principiante è facile annegare nelle formalità invece di capire l’essenza. Conviene attivarlo quando si è già pronti a mantenere il progetto «pulito» e si vuole disciplina. All’inizio è più ragionevole abituarsi a -Wall -Wextra, e usare -Werror in modo mirato.

Errore n. 4: compilare una parte dei .cpp con -O0 -g, un’altra con -O2, e poi stupirsi degli effetti strani.
Mescolare le modalità porta spesso a situazioni misteriose: da qualche parte una variabile «sparisce», altrove il comportamento è più difficile da diagnosticare, e a volte perfino gli avvisi sono diversi. È più stabile avere un unico set di flag per tutto il progetto e cambiarlo in blocco: «profilo di debug» oppure «profilo veloce».

Errore n. 5: pensare che -g sia la «modalità debug», e che -O2 sia la «modalità release», e che non serva altro.
In realtà sono due dimensioni indipendenti: -g riguarda le informazioni diagnostiche, mentre -O... riguarda l’ottimizzazione. Si può compilare anche con -O2 -g (a volte si fa davvero), ma è importante capire perché lo si sta facendo: le ottimizzazioni rendono meno «trasparente» ciò che accade, anche se i simboli ci sono.

1
Compito
C++ SELF, livello 33, lezione 3
Bloccato
Etichetta dello standard
Etichetta dello standard
1
Compito
C++ SELF, livello 33, lezione 3
Bloccato
Build pulita
Build pulita
1
Compito
C++ SELF, livello 33, lezione 3
Bloccato
Due profili
Due profili
Commenti
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION