Todo lo que necesitas saber sobre std :: any

Hola Habr! Le presentamos una traducción del artículo "Todo lo que necesita saber sobre std :: any de C ++ 17" de Bartlomiej Filipek .

imagen

Con la ayuda de std::optionalusted puede almacenar un tipo de tipo. Con la ayuda de std::variantusted puede almacenar varios tipos en un solo objeto. Y C ++ 17 nos proporciona otro tipo de envoltorio, std::anyque puede almacenar cualquier cosa sin dejar de ser seguro.

Los basicos


Antes de esto, el estándar C ++ no proporcionaba muchas soluciones al problema de almacenar varios tipos en una variable. Por supuesto que puede usar void*, pero no es del todo seguro.

Teóricamente, void*puede envolverlo en una clase donde de alguna manera puede almacenar el tipo:

class MyAny
{
    void* _value;
    TypeInfo _typeInfo;
};

Como puede ver, obtuvimos una cierta forma básica std::any, pero para garantizar la seguridad del tipo MyAnynecesitamos verificaciones adicionales. Es por eso que es mejor usar una opción de la biblioteca estándar que tomar su propia decisión.

Y esto es lo que es std::anyde C ++ 17. Le permite almacenar cualquier cosa en el objeto e informa un error (produce una excepción) cuando intenta acceder especificando el tipo incorrecto.

Pequeña demo:

std::any a(12);

//    :
a = std::string("Hello!");
a = 16;
//   :

//    a  
std::cout << std::any_cast<int>(a) << '\n'; 

//    :
try 
{
    std::cout << std::any_cast<std::string>(a) << '\n';
}
catch(const std::bad_any_cast& e) 
{
    std::cout << e.what() << '\n';
}

//        - :
a.reset();
if (!a.has_value())
{
    std::cout << "a is empty!" << "\n";
}

//    any  :
std::map<std::string, std::any> m;
m["integer"] = 10;
m["string"] = std::string("Hello World");
m["float"] = 1.0f;

for (auto &[key, val] : m)
{
    if (val.type() == typeid(int))
        std::cout << "int: " << std::any_cast<int>(val) << "\n";
    else if (val.type() == typeid(std::string))
        std::cout << "string: " << std::any_cast<std::string>(val) << "\n";
    else if (val.type() == typeid(float))
        std::cout << "float: " << std::any_cast<float>(val) << "\n";
}

Este código generará:

16
bad any_cast
a is empty!
float: 1
int: 10
string: Hello World

El ejemplo anterior muestra algunas cosas importantes:

  • std::anystd::optional std::variant
  • , .has_value()
  • .reset()
  • std::decay
  • ,
  • std::any_cast, bad_any_cast, «T»
  • .type(), std::type_info

El ejemplo anterior parece impresionante: ¡una variable de tipo real en C ++! Si realmente te gusta JavaScript, puedes incluso hacer todas tus variables de tipo std::anyy usar C ++ como JavaScript :) ¿

Pero quizás hay algunos ejemplos de uso normal?

¿Cuándo usar?


Si bien es void*percibido por mí como algo muy inseguro con una gama muy limitada de usos posibles, es std::anycompletamente seguro, por lo que tiene algunas buenas maneras de usarlo.

Por ejemplo:

  • En bibliotecas: cuando su biblioteca necesita almacenar o transferir algunos datos, y no sabe de qué tipo pueden ser estos datos
  • Al analizar archivos, si realmente no puede determinar qué tipos son compatibles
  • Mensajería
  • Interacción del lenguaje de scripting
  • Crear un intérprete para un lenguaje de script
  • Interfaz de usuario: los campos pueden almacenar cualquier cosa

Me parece que en muchos de estos ejemplos podemos resaltar una lista limitada de tipos admitidos, por lo que std::variantpuede ser una mejor opción. Pero, por supuesto, es difícil crear bibliotecas sin conocer los productos finales en los que se utilizará. Simplemente no sabe qué tipos se almacenarán allí.

La demostración mostró algunas cosas básicas, pero en las siguientes secciones aprenderá más std::any, así que siga leyendo.

Crear std :: any


Hay varias formas de crear un objeto de tipo std::any:

  • inicialización estándar: el objeto está vacío
  • inicialización directa con valor / objeto
  • indicando directamente el tipo de objeto - std::in_place_type
  • vía std::make_any

Por ejemplo:

//  :
std::any a;
assert(!a.has_value());

//   :
std::any a2(10); // int
std::any a3(MyType(10, 11));

// in_place:
std::any a4(std::in_place_type<MyType>, 10, 11);
std::any a5{std::in_place_type<std::string>, "Hello World"};

// make_any
std::any a6 = std::make_any<std::string>("Hello World");

Valor de cambio


Hay std::anydos formas de cambiar el valor almacenado actualmente : método emplaceo asignación:

std::any a;

a = MyType(10, 11);
a = std::string("Hello");

a.emplace<float>(100.5f);
a.emplace<std::vector<int>>({10, 11, 12, 13});
a.emplace<MyType>(10, 11);

Ciclo de vida del objeto


La clave para la seguridad std::anyes la falta de fuga de recursos. Para lograr esto, std::anydestruirá cualquier objeto activo antes de asignar un nuevo valor.

std::any var = std::make_any<MyType>();
var = 100.0f;
std::cout << std::any_cast<float>(var) << "\n";

Este código generará lo siguiente:

MyType::MyType
MyType::~MyType
100

El objeto se std::anyinicializa con un objeto de tipo MyType, pero antes de asignar un nuevo valor (100.0f), se llama al destructor MyType.

Obtener acceso a un valor


En la mayoría de los casos, solo tiene una forma de acceder al valor en std::any- std::any_cast, devuelve los valores del tipo especificado si está almacenado en el objeto.

Esta característica es muy útil, ya que tiene muchas formas de usarla:

  • devolver una copia del valor y salir std::bad_any_castpor error
  • devolver un enlace al valor y salir std::bad_any_cast por error
  • devolver un puntero a un valor (constante o no) o nullptr en caso de error

Ver un ejemplo:

struct MyType
{
    int a, b;

    MyType(int x, int y) : a(x), b(y) { }

    void Print() { std::cout << a << ", " << b << "\n"; }
};

int main()
{
    std::any var = std::make_any<MyType>(10, 10);
    try
    {
        std::any_cast<MyType&>(var).Print();
        std::any_cast<MyType&>(var).a = 11; // /
        std::any_cast<MyType&>(var).Print();
        std::any_cast<int>(var); // throw!
    }
    catch(const std::bad_any_cast& e) 
    {
        std::cout << e.what() << '\n';
    }

    int* p = std::any_cast<int>(&var);
    std::cout << (p ? "contains int... \n" : "doesn't contain an int...\n");

    MyType* pt = std::any_cast<MyType>(&var);
    if (pt)
    {
        pt->a = 12;
        std::any_cast<MyType&>(var).Print();
    }
}

Como puede ver, tenemos dos formas de rastrear errores: a través de excepciones ( std::bad_any_cast) o devolviendo un puntero (o nullptr). La función std::any_castpara devolver punteros está sobrecargada y marcada como noexcept.

Rendimiento y uso de memoria


std::anyParece una herramienta poderosa, y lo más probable es que la use para almacenar datos de diferentes tipos, pero ¿cuál es el precio?

El principal problema es la asignación de memoria adicional.

std::variant y std::optionalno requiere ninguna asignación de memoria adicional, pero esto se debe a que los tipos de datos almacenados en el objeto se conocen de antemano. std :: any no tiene dicha información, por lo que puede usar memoria adicional.

¿Esto sucederá siempre o algunas veces? Que reglas ¿Esto sucederá incluso con tipos simples como int?

Veamos qué dice el estándar:
Las implementaciones deben evitar el uso de memoria asignada dinámicamente para un pequeño valor contenido. Ejemplo: donde el objeto construido solo contiene un int. Dicha optimización de objetos pequeños solo se aplicará a los tipos T para los que is_nothrow_move_constructible_v es verdadero
Una implementación debe evitar el uso de memoria dinámica para datos almacenados de pequeño tamaño. Por ejemplo, cuando se crea un objeto que almacena solo int. Dicha optimización para objetos pequeños solo debe aplicarse a los tipos T para los que is_nothrow_move_constructible_v es verdadero.

Como resultado, proponen usar Small Buffer Optimization / SBO para implementaciones. Pero esto también tiene un precio. Esto hace que el tipo sea más grande, para cubrir el búfer.

Veamos el tamaño std::any, aquí están los resultados de varios compiladores:

Compiladortamaño de (cualquiera)
GCC 8.1 (Coliru)dieciséis
Clang 7.0.0 (Wandbox)32
MSVC 2017 15.7.0 32 bits40
MSVC 2017 15.7.0 64 bits64

En general, como puede ver, std::anyeste no es un tipo simple y conlleva costos adicionales. Por lo general, ocupa mucha memoria, debido a SBO, de 16 a 32 bytes (en GCC o clang ... ¡o incluso 64 bytes en MSVC!).

Migración desde boost :: any


boost::anyFue introducido en algún lugar en 2001 (versión 1.23.0). Además, el autor boost::any(Kevlin Henney) también es el autor de la propuesta std::any. Por lo tanto, estos dos tipos están estrechamente relacionados, la versión de STL está fuertemente basada en su predecesora.

Aquí están los principales cambios:

FunciónBoost.Any
(1.67.0)
std :: any
Asignación de memoria adicionalsisi
Optimización de objetos pequeñosNosi
lugarNosi
in_place_type_t en el constructorNosi


La principal diferencia es que boost::anyno usa SBO, por lo que ocupa significativamente menos memoria (en GCC8.1 su tamaño es de 8 bytes), pero debido a esto, asigna memoria de forma dinámica incluso para tipos tan pequeños como int.

Ejemplos de uso de std :: any


La principal ventaja std::anyes la flexibilidad. En los ejemplos a continuación, puede ver algunas ideas (o implementaciones específicas) donde su uso std::anyhace que la aplicación sea un poco más fácil.

Análisis de archivos


En los ejemplos de std::variant ( puede verlos aquí [eng] ) puede ver cómo puede analizar los archivos de configuración y almacenar el resultado en una variable de tipo std::variant. Ahora está escribiendo una solución muy general, tal vez es parte de alguna biblioteca, entonces es posible que no conozca todos los tipos posibles de tipos. Es probable que

almacenar datos utilizando std::anyparámetros sea bastante bueno en términos de rendimiento y, al mismo tiempo, le brinde la flexibilidad de la solución.

Mensajería


En Windows Api, que está escrito principalmente en C, hay un sistema de mensajería que utiliza la identificación del mensaje con dos parámetros opcionales que almacenan los datos del mensaje. Basado en este mecanismo, puede implementar WndProc, que procesa el mensaje enviado a su ventana.

LRESULT CALLBACK WindowProc(
  _In_ HWND   hwnd,
  _In_ UINT   uMsg,
  _In_ WPARAM wParam,
  _In_ LPARAM lParam
);

El hecho es que los datos se almacenan en wParamo lParamen diferentes formas. A veces solo necesitas usar un par de bytes wParam.

¿Qué pasa si cambiamos este sistema para que el mensaje pueda pasar cualquier cosa al método de procesamiento?

Por ejemplo:

class Message
{
public:
    enum class Type 
    {
        Init,
        Closing,
        ShowWindow,        
        DrawWindow
    };

public:
    explicit Message(Type type, std::any param) :
        mType(type),
        mParam(param)
    {   }
    explicit Message(Type type) :
        mType(type)
    {   }

    Type mType;
    std::any mParam;
};

class Window
{
public:
    virtual void HandleMessage(const Message& msg) = 0;
};

Por ejemplo, puede enviar un mensaje a la ventana:

Message m(Message::Type::ShowWindow, std::make_pair(10, 11));
yourWindow.HandleMessage(m);

Una ventana puede responder a un mensaje como este:

switch (msg.mType) {
// ...
case Message::Type::ShowWindow:
    {
    auto pos = std::any_cast<std::pair<int, int>>(msg.mParam);
    std::cout << "ShowWidow: "
              << pos.first << ", " 
              << pos.second << "\n";
    break;
    }
}

Por supuesto, debe determinar cómo se almacena el tipo de datos en los mensajes, pero ahora puede usar tipos reales en lugar de diferentes trucos con números.

Propiedades


El documento original que representa para C ++ (N1939) muestra un ejemplo de un objeto de propiedad:

struct property
{
    property();
    property(const std::string &, const std::any &);

    std::string name;
    std::any value;
};

typedef std::vector<property> properties;

Este objeto se ve muy útil porque puede almacenar muchos tipos diferentes. El primero que me viene a la mente es un ejemplo de su uso en un administrador de interfaz de usuario o en un editor de juegos.

Pasamos por las fronteras


En r / cpp había una secuencia sobre std :: any. Y hubo al menos un gran comentario que resume cuándo se debe usar un tipo.

De este comentario :
La conclusión es que std :: any le permite transferir derechos a datos arbitrarios a través de fronteras que no conocen su tipo.
Todo lo que hablé antes está cerca de esta idea:

  • en la biblioteca para la interfaz: no sabe qué tipos quiere usar el cliente allí
  • mensajería: la misma idea: dar flexibilidad al cliente
  • análisis de archivos: para admitir cualquier tipo

Total


¡En este artículo, aprendimos mucho std::any!

Aquí hay algunas cosas a tener en cuenta:

  • std::any no es una clase de plantilla
  • std::any utiliza la optimización de objetos pequeños, por lo que no asignará dinámicamente memoria para tipos tan simples como int o double, y para tipos más grandes, se usará memoria adicional
  • std::any puede llamarse "pesado", pero ofrece seguridad y mayor flexibilidad
  • El acceso a los datos std::anyse puede obtener con la ayuda de any_cast, que ofrece varios "modos". Por ejemplo, en caso de error, puede arrojar una excepción o simplemente devolver nullptr
  • úselo cuando no sepa exactamente qué tipos de datos son posibles; de lo contrario, considere usar std::variant

All Articles