CodeGym /Corsi /C++ SELF /static_assert nei template

static_assert nei template

C++ SELF
Livello 58, Lezione 0
Disponibile

1. Perché gli errori nei template sono «rumorosi»

Quando si scrive un template, in pratica si scrive «codice per il futuro»: il compilatore lo controllerà seriamente solo quando qualcuno fornirà un tipo concreto. Per questo l'errore spesso appare come una lunga sequenza di «sostituzione X», «istanza Y», «e qui al tipo manca il metodo size()». Vogliamo imparare a trasformare tutto ciò in una frase breve e comprensibile: «Questa funzione funziona solo per …».

Immaginate una situazione tipica: scrivete una funzione generica per i «numeri», e qualcuno (o voi stessi dopo una settimana) per sbaglio passa una std::string. Il compilatore prova onestamente a compilare il corpo, incappa in x * x, e comincia a raccontare la tragica storia di tutti i template coinvolti. Dal punto di vista del compilatore è corretto. Dal punto di vista dello studente — è «AAAAAA».

static_assert è il nostro modo per dire al compilatore: «Se la condizione non è soddisfatta, fermati qui e mostra il mio messaggio».

static_assert: idea di base e sintassi

static_assert è una verifica a tempo di compilazione. O passa «silenziosamente», oppure blocca la compilazione con un errore. A differenza di if, che funziona a runtime, static_assert è una vera e propria «guardia all'ingresso»: non permette al codice di proseguire se il contratto è violato.

La sintassi esiste in due forme: con messaggio e senza messaggio. Quasi mai si vuole usare la forma senza messaggio nel codice didattico, perché «errore senza testo» è come un biglietto «hai sbagliato» senza spiegazioni.


#include <concepts>

template <class T>
T square(T x) {
    static_assert(std::integral<T> || std::floating_point<T>,
                  "square<T>: T must be an integer or floating-point type");
    return x * x;
}

Qui l'idea è semplice: se qualcuno chiama square(std::string{"hi"}), il compilatore si fermerà e mostrerà esattamente la vostra frase.

2. Regola importante: static_assert scatta all'istanza

Quando si scrive una funzione normale (non template), il compilatore la controlla subito. Quando si scrive un template, il compilatore spesso rimanda il controllo del corpo al momento dell'utilizzo. Per questo il comportamento di static_assert nei template può sembrare magico, ma è una buona magia.

È importante capire lo schema semplice:

flowchart TD
    A[Template dichiarato] --> B{Esiste una chiamata con un tipo concreto?}
    B -- no --> C[Il corpo viene quasi per nulla controllato]
    B -- si --> D[Istanziamento]
    D --> E[Compilazione del corpo + static_assert]
    E -- condizione true --> F[Ok]
    E -- condizione false --> G[Errore con il tuo messaggio]

Quindi static_assert in un template è una «trappola» che scatterà proprio quando il template viene realmente applicato a un tipo non adatto. Ed è quello che desideriamo: un template non dovrebbe «rompere» il progetto solo per il fatto di esistere — deve rompersi solo se usato in modo errato.

4. Trappola: static_assert(false, "...") «per sicurezza»

Un errore comune del principiante è: «Voglio proibire una certa branca — scrivo static_assert(false, "…")». Nel codice normale questo fermerà immediatamente la compilazione (e tutto). Nel codice template questo fermerà comunque la compilazione — ma sempre, anche se non si usa il template. In pratica avete piazzato una «bomba nell'header».

Mostriamo il problema con un esempio breve:

template <class T>
void broken() {
    static_assert(false, "This template is forbidden"); // male
}

Questo è «male» perché false non dipende da T. Il compilatore vede false immediatamente e non è obbligato ad aspettare l'utilizzo: dice semplicemente «no» e chiude bottega.

Perché static_assert in un template sia utile, la sua condizione dovrebbe di solito dipendere dai parametri del template (ad esempio da T). Così la verifica viene rimandata al momento dell'effettivo utilizzo.

5. Trick always_false_v<T> per la «branch proibita»

A volte serve davvero dire: «Se sei qui, stai usando un tipo non supportato». Esiste un trucco molto popolare e semplice: creare una «falsa» che dipende dal tipo.

Ecco lo scheletro minimo:

template <class>
inline constexpr bool always_false_v = false;

Ora si può scrivere:

#include <concepts>

template <class T>
void print_value(const T&) {
    static_assert(always_false_v<T>,
                  "print_value<T>: unsupported type (add an overload or constrain T)");
}

Qui il compilatore non si lamenta «subito», perché always_false_v<T> dipende da T. L'errore apparirà solo se qualcuno invoca realmente print_value con un tipo non supportato.

Questo stratagemma è particolarmente utile nei fallback, quando si scrivono più overload per tipi diversi e nell'ultimo si vuole dire: «no, questo tipo non lo sappiamo gestire».

6. Dove mettere static_assert: più vicino all'input

Spesso viene la tentazione di mettere la verifica «sul posto», vicino alla riga problematica (x.size(), a + b, x * x). Ma per l'UX (e per la vostra futura salute mentale) è più utile collocare static_assert all'inizio di funzione/classe. Così, in caso di errore, l'utente vede: «la funzione X richiede questo», invece di «qualcosa è andato storto più in basso».

Confrontate due stili. Il primo stile peggiora la leggibilità dell'errore, perché la verifica è lontana:

#include <concepts>

template <class T>
T add_late(T a, T b) {
    T c = a + b; // l'errore qui può generare una "lista infinita"
    static_assert(std::integral<T> || std::floating_point<T>,
                  "add_late<T>: T must be numeric");
    return c;
}

Il secondo stile è di solito migliore:

#include <concepts>

template <class T>
T add_early(T a, T b) {
    static_assert(std::integral<T> || std::floating_point<T>,
                  "add_early<T>: T must be numeric");
    return a + b;
}

Sì, il compilatore potrebbe comunque intuire che il problema è in a + b. Ma noi lo aiutiamo ad aiutare la persona.

7. static_assert insieme a concept: spiegare i requisiti

I vincoli tramite concept e requires spesso forniscono già una diagnostica abbastanza buona. Però a volte si vuole spiegare l'errore con parole proprie, soprattutto se si sta scrivendo una libreria didattica o codice interno dove la chiarezza è importante.

Un approccio pratico: limitare l'interfaccia con un concept e comunque lasciare un static_assert all'interno — o per chiarire, o per un testo più amichevole. È importante non trasformare questo in duplicazione fine a sé stessa: se il messaggio del compilatore è già ottimo, un static_assert in più può solo fare rumore.

Esempio nello spirito di un'app didattica (CLI per il tracciamento task): vogliamo parsare un id numerico da una stringa.

#include <charconv>
#include <concepts>
#include <string_view>

template <std::integral T>
T parse_int(std::string_view s) {
    T value{};
    auto [ptr, ec] = std::from_chars(s.data(), s.data() + s.size(), value);
    static_assert(std::integral<T>, "parse_int<T>: T must be integral"); // spiegazione
    (void)ptr; (void)ec;
    return value;
}

Dal punto di vista logico puro il static_assert qui è ridondante (dato che T è già std::integral). Ma a fini didattici può essere utile lasciarlo come «faro»: lo studente vede che la funzione si aspetta davvero un tipo intero, non «qualcosa di numerico in generale».

Spesso più utile è un'altra variante: il concept fa un filtro grossolano e il static_assert precisa il contratto. Per esempio, «il tipo deve essere numerico e ci aspettiamo che la divisione abbia senso».

#include <concepts>

template <class T>
T average2(T a, T b) {
    static_assert(std::floating_point<T>,
                  "average2<T>: use floating point type (e.g. double) to avoid integer division");
    return (a + b) / static_cast<T>(2);
}

8. Esempio pratico: migliorare la diagnostica in TaskBoard

Per evitare che static_assert resti una «filosofia astratta», immaginiamo, nel contesto del corso, di sviluppare un'applicazione — il tracker di task in console TaskBoard. In fasi precedenti abbiamo già memorizzato task in std::vector, letto comandi da stringhe, parsato con std::stringstream e validato l'input. Ora vogliamo creare una piccola utility di parsing riutilizzabile.

Consideriamo un comando del tipo:

add 101 "Buy milk"

101 è l'id. Qui vogliamo due cose: una funzione di parsing generale comoda e un errore di compilazione chiaro se il programmatore la usa in modo sbagliato.

Creiamo una piccola funzione parse_id<T> pensata solo per interi:

#include <charconv>
#include <concepts>
#include <string_view>

template <class T>
T parse_id(std::string_view s) {
    static_assert(std::integral<T>,
                  "parse_id<T>: T must be an integral type (int, long long, ...)");
    T id{};
    std::from_chars(s.data(), s.data() + s.size(), id);
    return id;
}

Ora nel codice di gestione del comando si può indicare esplicitamente il tipo desiderato:

#include <iostream>
#include <string>

int main() {
    std::string token = "101";
    int id = parse_id<int>(token);
    std::cout << "id=" << id << '\n'; // id=101
}

Se qualcuno prova a scrivere auto id = parse_id<std::string>("101");, il compilatore si fermerà mostrando esattamente il vostro messaggio. Non «perché non si può usare from_chars con string», non «nessun overload adatto», ma una spiegazione umana: «T must be integral».

Nota importante sul design del messaggio: dovrebbe rispondere a due domande. La prima — cosa non va (per esempio «T must be integral»). La seconda — cosa fare invece (per esempio «use int/long long» o «scrivi un altro overload»). Senza la seconda parte il static_assert diventa un «colpa tua», mentre vogliamo aiutare a correggere.

Concept vs static_assert: chi fa cosa

A volte gli studenti si confondono: «Se ci sono i concepts, a che serve static_assert?» La domanda è legittima, perché entrambi trattano restrizioni. Però i loro ruoli sono distinti.

Strumento Dove vive Cosa fa meglio Come appare solitamente l'errore
concept / requires
In firma Escludere il tipo non adatto prima dell'analisi del corpo; rendere il contratto visibile leggendo l'API «does not satisfy constraint …» (spesso già accettabile)
static_assert
Dentro il template (spesso all'inizio) Fornire il vostro messaggio; precisare i requisiti; dare «una ragione chiara» invece della cascata di errori Il testo che avete scritto

Nel codice didattico spesso è utile avere entrambi: i constraint rendono l'API onesta, e static_assert rende la diagnostica più amichevole. In produzione reale si dosano i static_assert per non duplicare constraint già efficaci.

9. Errori tipici usando static_assert nei template

Errore №1: static_assert(false, "...") dentro un template e la sorpresa «perché non compila nulla».
Succede perché false non dipende dai parametri del template, e il compilatore ha il diritto di «buttare tutto giù» subito, anche se non usate mai quel template. La soluzione corretta è rendere la condizione dipendente da T, per esempio tramite always_false_v<T> o una verifica reale come std::integral<T>.

Errore №2: messaggio del tipo "Type error" o "Bad type".
Formalmente è «umano», ma praticamente inutile: non spiega cosa ci si aspettava e come correggere. Meglio scrivere «nome funzione: requisito + suggerimento», per esempio: "parse_id<T>: T must be integral (use int/long long)".

Errore №3: static_assert troppo in profondità nel codice, e l'utente vede prima la «lista infinita» e poi il vostro messaggio.
Se la verifica è dopo molte righe di logica template complessa, il compilatore può accumulare errori secondari. Alla fine il static_assert non salva l'UX. Il modo più semplice per migliorare è mettere static_assert all'inizio della funzione/classe come controllo del contratto in ingresso.

Errore №4: requisiti eccessivamente severi «per sicurezza».
A volte si vuole richiedere da T tutto: addizione, moltiplicazione, size(), essere copiabile, essere stampabile su cout. Così si restringe molto l'applicabilità del template e poi ci si lamenta quando un tipo apparentemente adatto non passa il filtro. Nella buona formulazione sia di static_assert che dei constraint bisogna richiedere solo ciò che è realmente necessario al corpo della funzione o al contratto dell'API.

Errore №5: confondere le aspettative — usare static_assert come validazione a runtime.
static_assert funziona solo a tempo di compilazione. Non controllerà l'input dell'utente, non rileverà «stringa vuota», non salverà da un errore restituito da from_chars. Protegge da tipi errati e da contratti d'uso non rispettati. Per input e dati abbiamo controlli di stato dei stream, optional/expected, codici di errore e i classici if — ma questi sono altri strumenti.

1
Compito
C++ SELF, livello 58, lezione 0
Bloccato
Quadrato onesto
Quadrato onesto
1
Compito
C++ SELF, livello 58, lezione 0
Bloccato
Debug vietato
Debug vietato
1
Compito
C++ SELF, livello 58, lezione 0
Bloccato
Stampa della dimensione
Stampa della dimensione
1
Compito
C++ SELF, livello 58, lezione 0
Bloccato
Convertitore di testo
Convertitore di testo
Commenti
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION