std::variant [C++17] es una unión tipada (type-safe union) que actúa como un contenedor que almacena exactamente uno de los tipos listados en su definición. A diferencia de la union clásica de C, que es simplemente un bloque de memoria donde diferentes tipos comparten la misma dirección, std::variant mantiene un discriminador interno (un índice) para saber qué tipo es el activo en cada momento. Esto garantiza que la gestión de la vida de los objetos sea correcta: cuando cambias el tipo almacenado, el variant llama automáticamente al destructor del tipo anterior y ejecuta el constructor del nuevo.
Esta estructura es fundamental para implementar máquinas de estado o representar resultados que pueden ser un valor o un error, sin recurrir a punteros o polimorfismo basado en herencia. Para usarla, necesitas saber que std::variant nunca está “vacía” por defecto; siempre debe contener un objeto. Si necesitas representar un estado “sin valor” o un estado inicial, debes incluir std::monostate como uno de los tipos permitidos. El acceso se realiza mediante std::get<T> (que lanza std::bad_variant_access si el tipo no coincide) o std::get_if<T>, que devuelve un puntero al valor o nullptr si el tipo es incorrecto, permitiendo un control de flujo más seguro y eficiente. Si intentas acceder mediante std::get al tipo equivocado en un entorno crítico, tu programa lanzará una excepción; si no gestionas el estado inicial correctamente con std::monostate, podrías encontrarte con errores de compilación si los tipos alternativos no tienen constructores por defecto.
#include <iostream>
#include <variant>
#include <string>
#include <vector>
// Patrón overloaded para usar lambdas con std::visit
template<class... Ts> struct overloaded : Ts... { using Ts::operator()...; };
template<class... Ts> overloaded(Ts...) -> overloaded<Ts...>;
// Definimos un tipo de dato que puede ser un estado inicial,
// un valor entero (ID), un valor flotante (medición) o un error.
using SensorData = std::variant<std::monostate, int, double, std::string>;
void procesar_sensor(const SensorData& data) {
// std::visit es la forma más potente y segura de trabajar con variant.
// El compilador genera una tabla de saltos (jump table) basada en el índice
// interno, lo que es mucho más eficiente que una cadena de if-else.
std::visit(overloaded {
[](std::monostate) {
std::cout << "Estado: Sensor sin inicializar.\n";
},
[](int id) {
std::cout << "Modo ID: " << id << "\n";
},
[](double valor) {
std::cout << "Medición: " << valor << " unidades.\n";
},
[](const std::string& error) {
std::cerr << "Error crítico: " << error << "\n";
}
}, data);
}
int main() {
// 1. Inicialización con std::monostate (estado vacío)
SensorData sensor = std::monostate{};
procesar_sensor(sensor);
// 2. Asignación de un tipo distinto (cambia el discriminador internamente)
sensor = 42;
procesar_sensor(sensor);
sensor = 23.5;
procesar_sensor(sensor);
sensor = std::string("Fallo de comunicación");
procesar_sensor(sensor);
// 3. Uso de std::get_if para acceso seguro sin excepciones
// std::get_if devuelve un puntero al valor contenido si el tipo coincide.
if (auto* p_val = std::get_if<double>(&sensor)) {
std::cout << "Verificación rápida de double: " << *p_val << "\n";
} else {
std::cout << "El sensor no es un double en este momento.\n";
}
// 4. Comprobación de tipo con std::holds_alternative
if (std::holds_alternative<std::string>(sensor)) {
std::cout << "El sensor está actualmente en estado de error.\n";
}
return 0;
}
En el ejemplo anterior, SensorData utiliza std::monostate para permitir que el variant se construya con un valor por defecto que no requiere lógica de inicialización compleja. Cuando ejecutamos sensor = 42;, el variant destruye el objeto anterior (si lo hubiera) y utiliza su discriminador para marcar que el tipo activo es el segundo de la lista (int).
Al llamar a procesar_sensor, utilizamos std::visit junto con la estructura overloaded. Esta técnica es crucial: el compilador inspecciona todas las lambdas proporcionadas y asegura que cubras todos los tipos posibles definidos en SensorData. Si olvidaras un tipo, el código no compilaría. Internamente, std::visit evita el uso de typeid y RTTI (Run-Time Type Information) pesado, utilizando en su lugar el índice almacenado para realizar un salto directo a la función correspondiente.
En la sección de main, observamos la diferencia operativa entre std::get_if y el acceso directo. std::get_if<double>(&sensor) no lanza excepciones; simplemente devuelve nullptr si el sensor contiene un std::string. Esto es preferible en bucles de alta frecuencia donde el error es una posibilidad esperada y no una excepción de flujo de control.
El error frecuente
Un error común es el uso de std::get<T> cuando no se tiene la certeza absoluta del tipo activo, asumiendo que el programador “sabe” qué hay dentro.
std::variant<int, std::string> v = "Error"; // Esto lanza std::bad_variant_access porque el tipo activo es std::string, no int. int valor = std::get<int>(v);
Si este código se ejecuta, la excepción std::bad_variant_access será lanzada. En sistemas de tiempo real o de alto rendimiento, donde las excepciones están deshabilitadas (-fno-exceptions) o se consideran demasiado costosas por su impacto en el determinismo, usar std::get de forma insegura provocará un error de ejecución inmediato. En esos escenarios, es obligatorio usar std::get_if o verificar primero con std::holds_alternative. AddressSanitizer no detectará esto como un error de memoria, ya que es un comportamiento definido por el estándar, pero es un error de lógica de ejecución que detendrá tu proceso.
N° 90