Plantillas de clase: `std::variant` y `std::visit`
std::variant
En C++17 se introdujo la plantilla de clase std::variant, que permite almacenar uno de varios tipos posibles, garantizando seguridad de tipo en tiempo de compilación.
Un std::variant puede considerarse como un contenedor de valor alternativo:
solo puede contener un valor activo a la vez, pero el tipo de ese valor puede ser uno de los declarados en su definición.
Por ejemplo, un std::variant<int, std::string> puede almacenar un int o una std::string, pero nunca ambos al mismo tiempo.
El compilador controla qué operaciones son válidas en función del tipo actualmente almacenado, lo que evita errores y elimina la necesidad de comprobaciones manuales en tiempo de ejecución.
std::variant es especialmente útil cuando un objeto o función debe manejar valores de diferentes tipos de manera flexible, manteniendo seguridad, eficiencia y expresividad en el código.
#include <iostream>
#include <variant>
int main() {
// Un std::variant que puede contener un int o un float
std::variant<int, float> valor;
valor = 10; // Contiene un int
std::cout << "Valor entero: " << std::get<int>(valor) << '\n';
valor = 3.14f; // Ahora contiene un float
std::cout << "Valor flotante: " << std::get<float>(valor) << '\n';
return 0;
}
- En este ejemplo,
valorpuede guardar uninto unfloat, pero nunca ambos a la vez. - El tipo activo se rastrea automáticamente en tiempo de compilación.
std::get<int>(variant)devuelve el valor almacenado en el std::variant si el tipo activo es int, lanzando una excepción si no coincide.
std::visit
Cuando un std::variant puede contener distintos tipos, necesitamos una forma segura y unificada de acceder al valor almacenado, sin importar cuál sea su tipo actual. std::visit recibe una función (o un objeto función) y un std::variant y ejecuta automáticamente esa función sobre el valor actual del variant, llamando a la versión que corresponde al tipo real que contiene en ese momento.
#include <iostream>
#include <variant>
#include <string>
int main() {
// std::variant puede contener un valor de cualquiera de los tipos listados
std::variant<int, std::string> dato;
// Asignamos inicialmente una cadena
dato = "Hola mundo";
// Primer uso de std::visit:
// std::visit aplica una función (lambda, en este caso)
// al valor actualmente almacenado en el variant
std::visit([](auto&& valor) {
std::cout << "Valor inicial: " << valor << '\n';
}, dato);
// Ahora cambiamos el contenido del variant a un entero
dato = 42;
// Segundo uso de std::visit:
// Se vuelve a ejecutar la misma función sobre el nuevo valor
std::visit([](auto&& valor) {
std::cout << "Nuevo valor: " << valor << '\n';
}, dato);
// Podemos comprobar qué tipo contiene actualmente el variant
if (std::holds_alternative<int>(dato)) {
std::cout << "El variant contiene un entero.\n";
} else if (std::holds_alternative<std::string>(dato)) {
std::cout << "El variant contiene una cadena.\n";
}
// Acceder directamente al valor si sabemos su tipo
std::cout << "Acceso directo al entero: " << std::get<int>(dato) << '\n';
return 0;
}
std::variant<int, std::string>: Define un tipo que puede contener un entero o una cadena (solo uno a la vez).std::visit(): Aplica una función a lo que haya dentro delvariant, sin importar el tipo actual.- La lambda genérica
[](auto&& valor)se adapta automáticamente al tipo real del valor:autodeduce el tipo, y&&permite recibir el valor sin copiarlo (igual que en el movimiento). std::holds_alternative<T>(v): Permite comprobar si elvariantcontiene un valor de tipoT.std::get<T>(v): Devuelve el valor si el tipo actual esT, o lanza una excepciónstd::bad_variant_accesssi no coincide.
Ejemplo completo: procesar distintos tipos de eventos
Supongamos una aplicación que puede recibir varios eventos: un número de control (int), un mensaje (std::string) o una lectura de sensor (double). Queremos procesar cada tipo de manera diferente.
#include <iostream>
#include <variant>
#include <string>
#include <vector>
// Tipo variante que puede contener uno de varios tipos
using Evento = std::variant<int, std::string, double>;
// Visitor con sobrecarga de funciones
class ProcesadorEvento {
public:
void operator()(int codigo) const {
std::cout << "Código de control recibido: " << codigo << '\n';
}
void operator()(const std::string& mensaje) const {
std::cout << "Mensaje recibido: \"" << mensaje << "\"\n";
}
void operator()(double lectura) const {
std::cout << "Lectura de sensor: " << lectura << " unidades\n";
}
};
int main() {
std::vector<Evento> eventos = {
42,
std::string("Arranque del sistema"),
23.7,
std::string("Advertencia: temperatura alta"),
0,
18.2
};
// Procesar cada evento según su tipo
for (const auto& evento : eventos) {
std::visit(ProcesadorEvento{}, evento);
}
return 0;
}
std::variant<int, std::string, double>define un tipo que puede contener uno solo de esos tres tipos a la vez.ProcesadorEventoes una clase visitor que sobrecargaoperator()para cada tipo posible.std::visit(ProcesadorEvento{}, evento)llama automáticamente a la versión correcta del método según el tipo almacenado.- Así, se evita el uso de casts o comprobaciones manuales de tipo.
- Si todos los tipos pudieran procesarse de la misma forma, se podría usar directamente una lambda genérica, pero cuando el comportamiento depende del tipo concreto, un visitor con sobrecargas es más claro y seguro.