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 | |
| Build veloce | esecuzione normale, velocità | |
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 |
|---|---|---|
|
regole del linguaggio e disponibilità di parte della libreria | «compiliamo tutto con un unico standard, senza sorprese» |
|
set di avvisi | «nota ciò che è evidentemente sospetto» |
|
avvisi aggiuntivi | «nota ciò che è meno evidente» |
|
avvisi su elementi non standard | «non abituarti alle estensioni come se fossero la norma» |
|
avvisi → errori | «nel progetto non ci sono avvisi, punto» |
|
ottimizzazione disattivata | «diagnostica e comprensione più semplici, più lento» |
|
ottimizzazione forte | «di solito più veloce, ma il debug è meno trasparente» |
|
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.
GO TO FULL VERSION