Uso de std::any para el almacenamiento de tipos dinámicos

std::any es un contenedor de type erasure (borrado de tipo) que te permite almacenar un objeto de cualquier tipo que sea copiable, sin que el tipo del objeto sea parte de la firma de std::any. Internamente, el contenedor utiliza un mecanismo de herencia implícita y punteros a una interfaz abstracta para “olvidar” el tipo original y tratarlo de forma uniforme, almacenándolo en una región de memoria que puede ser el stack (si el tipo es pequeño, gracias a Small Buffer Optimization o SBO) o el heap (si el objeto es grande). Para recuperar el valor, necesitas conocer el tipo exacto; si usas std::any_cast<T>(objeto), el compilador verificará el tipo en tiempo de ejecución mediante RTTI (Run-Time Type Information) y lanzará una excepción std::bad_any_cast si fallas. Si prefieres evitar excepciones, puedes usar std::any_cast<T>(&objeto), que devuelve un puntero al contenido o nullptr si el tipo no coincide.

Utiliza std::any únicamente cuando la variabilidad de tipos sea absoluta y desconocida en tiempo de compilación, como en sistemas de propiedades para motores de scripting, motores de renderizado con materiales de tipos arbitrarios o sistemas de serialización genérica donde un nodo puede ser cualquier cosa. Si conoces de antemano un conjunto limitado de tipos posibles, usa std::variant para evitar el overhead de la asignación dinámica y la dependencia de RTTI. Ten cuidado: si intentas acceder a un tipo mediante un cast incorrecto, el programa lanzará una excepción o, en el caso de los punteros, te obligará a gestionar un nullptr, lo que puede llevar a fallos si no se valida correctamente.

#include <iostream>
#include <any>
#include <string>
#include <vector>
#include <typeinfo>

// Un tipo personalizado para demostrar el almacenamiento de estructuras
struct SensorData {
    double value;
    int timestamp;
};

void print_any_info(const std::any& a) {
    if (!a.has_value()) {
        std::cout << "Contenedor vacío.\n";
        return;
    }

    // Consultamos el tipo almacenado mediante typeid (requiere RTTI)
    std::cout << "Tipo almacenado: " << a.type().name() << " | ";

    // Intentamos extraer el valor de forma segura usando la versión de puntero
    // Esta versión no lanza excepciones, devuelve nullptr si el tipo no coincide.
    if (auto p = std::any_cast<int>(&a)) {
        std::cout << "Es un int: " << *p << "\n";
    } else if (auto p = std::any_cast<std::string>(&a)) {
        std::cout << "Es un std::string: " << *p << "\n";
    } else if (auto p = std::any_cast<SensorData>(&a)) {
        std::cout << "Es SensorData: val=" << p->value << ", ts=" << p->timestamp << "\n";
    } else if (auto p = std::any_cast<std::vector<double>>(&a)) {
        std::cout << "Es un std::vector<double> de tamaño " << p->size() << "\n";
    } else {
        std::cout << "Tipo desconocido para la lógica de impresión.\n";
    }
}

int main() {
    // 1. Construcción directa y SBO (Small Buffer Optimization)
    // El int suele almacenarse directamente en el objeto std::any (stack)
    std::any data = 42;
    print_any_info(data);

    // 2. Uso de std::make_any para mayor eficiencia y claridad
    // Evita la construcción de un objeto temporal
    data = std::make_any<std::string>("C++ Runtime");
    print_any_info(data);

    // 3. Almacenamiento de un tipo complejo (probablemente en el heap)
    data = SensorData{23.5, 1672531200};
    print_any_info(data);

    // 4. Almacenamiento de un contenedor de la STL
    data = std::vector<double>{1.1, 2.2, 3.3};
    print_any_cast<std::vector<double>>(data); // Esto sería un error si no fuera un cast de referencia
    print_any_info(data);

    // 5. Resetear el contenedor
    data.reset();
    print_any_info(data);

    return 0;
}

(Nota: Compila con g++ -std=c++20 -Wall -Wextra -Wpedantic -o example example.cpp)

Desglose del código

En main, empezamos con std::any data = 42;. Al ser un int, la implementación de la librería estándar suele aprovechar el Small Buffer Optimization (SBO), copiando el valor directamente dentro de la memoria interna de std::any sin llamar a new. Esto es crucial para el rendimiento en tipos pequeños.

Cuando hacemos data = std::make_any<std::string>("C++ Runtime");, std::make_any es preferible porque permite construir el objeto directamente en el almacenamiento de std::any, optimizando la transferencia de recursos. Al ser un std::string, es probable que el objeto exceda el tamaño del buffer interno, provocando una asignación en el heap.

En la función print_any_info, la llamada a a.type() utiliza la información de tipo en tiempo de ejecución (RTTI) para obtener un std::type_info. Fíjate en la técnica de extracción: usamos std::any_cast<T>(&a). Esta es la forma más robusta de trabajar en producción porque, al pasarle la dirección de a, el operador devuelve un puntero al tipo T*. Si el tipo contenido no es exactamente T, devuelve nullptr. Esto es preferible a usar std::any_cast<T>(a), ya que la versión por valor lanzaría una excepción std::bad_any_cast si fallara, lo cual es costoso y rompe el flujo de ejecución normal.

El error frecuente

Un error muy común es asumir que std::any_cast realiza conversiones implícitas. Si almacenas un std::string y tratas de extraerlo como const char*, el cast fallará, incluso si el tipo es convertible.

std::any a = std::string("Hola");

// ERROR: std::any_cast no realiza conversiones implícitas de tipos.
// Esto lanzará std::bad_any_cast.
try {
    const char* s = std::any_cast<const char*>(a); 
} catch (const std::bad_any_cast& e) {
    // El error es: 'std::string' no es 'const char*'
}

// FORMA CORRECTA:
const char* s_ok = std::any_cast<std::string>(&a) ? std::any_cast<std::string>(&a)->c_str() : nullptr;

Este comportamiento es estricto por diseño para mantener la integridad del type erasure. Si necesitas polimorfismo de tipos, usa std::variant o herencia tradicional; std::any es para cuando el tipo es un completo misterio.

91

Dejar un comentario

Tu dirección de correo electrónico no será publicada. Los campos obligatorios están marcados con *

Scroll al inicio