I tipi vocabolario: optional, variant e any
Il C++ moderno ha arricchito la libreria standard con una famiglia di tipi detti vocabolario, pensati per esprimere in modo chiaro concetti ricorrenti nella progettazione del software: un valore che potrebbe mancare, un valore che può assumere una tra più forme, un contenitore capace di conservare qualsiasi tipo. Questi tipi, introdotti con C++17 nelle rispettive intestazioni, sostituiscono soluzioni artigianali e insidiose con strumenti sicuri e uniformi, migliorando la leggibilità e la robustezza del codice.
std::optional: un valore che potrebbe mancare
Capita spesso che una funzione possa non avere un risultato da restituire: una ricerca che non trova nulla, una conversione che fallisce, un valore di configurazione assente. In passato si ricorreva a puntatori nulli, a codici di errore o a valori sentinella, tutte soluzioni fragili. Il tipo std::optional rappresenta esplicitamente la possibile assenza di un valore, rendendo l'intento evidente e obbligando chi lo usa a considerare il caso vuoto.
#include <optional>
#include <string>
#include <iostream>
// Restituisce un valore solo se l'input e valido
std::optional<int> parsePositive(int value) {
if (value > 0) {
return value;
}
return std::nullopt; // Assenza esplicita di valore
}
int main() {
auto result = parsePositive(42);
if (result.has_value()) {
std::cout << result.value() << std::endl;
}
return 0;
}
Il valore contenuto si verifica con has_value e si estrae con value. Un metodo particolarmente comodo è value_or, che restituisce il valore se presente oppure un valore predefinito fornito come argomento, evitando controlli espliciti.
std::optional<int> maybeValue = std::nullopt;
// Restituisce 0 perche il valore e assente
int safe = maybeValue.value_or(0);
std::variant: un valore tra più tipi
A volte una variabile deve poter contenere un valore che appartiene a uno tra diversi tipi possibili, ma uno solo per volta. Il tipo std::variant modella questa situazione in modo sicuro, sostituendo le vecchie unioni del C, che non tenevano traccia del tipo effettivamente contenuto ed erano fonte di errori. Un variant conosce sempre quale alternativa detiene in un dato momento.
#include <variant>
#include <string>
#include <iostream>
int main() {
// Puo contenere un intero oppure una stringa
std::variant<int, std::string> data;
data = 10; // Ora contiene un intero
data = "testo"; // Ora contiene una stringa
// Verifica quale alternativa e attiva
if (std::holds_alternative<std::string>(data)) {
std::cout << std::get<std::string>(data) << std::endl;
}
return 0;
}
La funzione std::holds_alternative verifica quale tipo è attualmente contenuto, mentre std::get ne estrae il valore, lanciando un'eccezione se il tipo richiesto non corrisponde a quello presente.
Visitare un variant
Il modo idiomatico di elaborare un variant è la funzione std::visit, che applica un oggetto richiamabile all'alternativa attualmente contenuta, selezionando automaticamente il comportamento corretto in base al tipo. Questo approccio è più sicuro e leggibile di una catena di controlli espliciti sul tipo.
#include <variant>
#include <iostream>
int main() {
std::variant<int, double> value = 3.5;
// La lambda generica gestisce ogni tipo possibile
std::visit([](auto content) {
std::cout << content << std::endl;
}, value);
return 0;
}
std::any: un contenitore per qualsiasi tipo
Quando serve conservare un valore di tipo non noto in anticipo, senza vincolarlo a un insieme predefinito di alternative, si può usare std::any. A differenza del variant, che ammette solo i tipi elencati, any può contenere qualunque tipo, al prezzo di una maggiore genericità e della necessità di conoscere il tipo esatto al momento dell'estrazione, che avviene con std::any_cast.
#include <any>
#include <string>
#include <iostream>
int main() {
std::any box = std::string("contenuto");
// L'estrazione richiede il tipo esatto
std::string text = std::any_cast<std::string>(box);
std::cout << text << std::endl;
return 0;
}
Quando usare ciascun tipo
La scelta tra questi tipi dipende dall'intento. Si usa std::optional per rappresentare la presenza o l'assenza di un singolo valore. Si usa std::variant quando un valore appartiene a un insieme chiuso e noto di tipi alternativi, ottenendo controlli di sicurezza in fase di compilazione. Si ricorre a std::any solo nei rari casi in cui l'insieme dei tipi possibili è realmente aperto, accettandone la minore sicurezza. Questi tipi vocabolario esprimono con precisione idee che altrimenti richiederebbero costrutti fragili, e la loro adozione rende il codice più dichiarativo e meno soggetto a errori.