Hola Habr! Le presentamos una traducción del artículo "Todo lo que necesita saber sobre std :: any de C ++ 17" de Bartlomiej Filipek .
Con la ayuda de std::optional
usted puede almacenar un tipo de tipo. Con la ayuda de std::variant
usted puede almacenar varios tipos en un solo objeto. Y C ++ 17 nos proporciona otro tipo de envoltorio, std::any
que 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 MyAny
necesitamos 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::any
de 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;
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";
}
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::any
— std::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::any
y 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::any
completamente 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::variant
puede 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);
std::any a3(MyType(10, 11));
std::any a4(std::in_place_type<MyType>, 10, 11);
std::any a5{std::in_place_type<std::string>, "Hello World"};
std::any a6 = std::make_any<std::string>("Hello World");
Valor de cambio
Hay std::any
dos formas de cambiar el valor almacenado actualmente : método emplace
o 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::any
es la falta de fuga de recursos. Para lograr esto, std::any
destruirá 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::any
inicializa 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_cast
por 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);
}
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_cast
para devolver punteros está sobrecargada y marcada como noexcept
.Rendimiento y uso de memoria
std::any
Parece 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::optional
no 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:En general, como puede ver, std::any
este 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::any
Fue 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:La principal diferencia es que boost::any
no 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::any
es la flexibilidad. En los ejemplos a continuación, puede ver algunas ideas (o implementaciones específicas) donde su uso std::any
hace 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 quealmacenar datos utilizando std::any
pará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 wParam
o lParam
en 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 plantillastd::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 adicionalstd::any
puede llamarse "pesado", pero ofrece seguridad y mayor flexibilidad- El acceso a los datos
std::any
se 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