Olá Habr! Apresentamos a sua atenção uma tradução do artigo “Tudo o que você precisa saber sobre std :: any from C ++ 17”, de Bartlomiej Filipek .
Com a ajuda de std::optional
você pode armazenar um tipo de tipo. Com a ajuda de std::variant
você pode armazenar vários tipos em um objeto. E o C ++ 17 nos fornece outro tipo de invólucro - std::any
que pode armazenar qualquer coisa enquanto permanece seguro para o tipo.O básico
Antes disso, o padrão C ++ não fornecia muitas soluções para o problema de armazenar vários tipos em uma variável. Claro que você pode usar void*
, mas não é nada seguro.Teoricamente, void*
você pode agrupá-lo em uma classe onde, de alguma forma, você pode armazenar o tipo:class MyAny
{
void* _value;
TypeInfo _typeInfo;
};
Como você pode ver, temos uma certa forma básica std::any
, mas para garantir a segurança do tipo MyAny
, precisamos de verificações adicionais. É por isso que é melhor usar uma opção da biblioteca padrão do que tomar sua própria decisão.E é isso que é std::any
do C ++ 17. Ele permite armazenar qualquer coisa no objeto e relata um erro (gera uma exceção) quando você tenta acessar, especificando o tipo errado.Pequena demonstração: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 produzirá:16
bad any_cast
a is empty!
float: 1
int: 10
string: Hello World
O exemplo acima mostra algumas coisas importantes:std::any
— std::optional
std::variant
- ,
.has_value()
.reset()
std::decay
- ,
std::any_cast
, bad_any_cast
, «T».type()
, std::type_info
O exemplo acima parece impressionante - uma variável de tipo real em C ++! Se você gosta muito de JavaScript, pode até criar todas as suas variáveis de tipo std::any
e usar C ++ como JavaScript :)Mas talvez haja alguns exemplos de uso normal?Quando usar?
Embora seja void*
percebido por mim como algo muito inseguro, com uma gama muito limitada de usos possíveis, é std::any
totalmente seguro para o tipo, portanto, existem algumas boas maneiras de usá-lo.Por exemplo:- Nas bibliotecas - quando sua biblioteca precisa armazenar ou transferir alguns dados e você não sabe de que tipo esses dados podem ser
- Ao analisar arquivos - se você realmente não pode determinar quais tipos são suportados
- Mensagens
- Interação com a linguagem de script
- Criando um intérprete para uma linguagem de script
- Interface do usuário - os campos podem armazenar qualquer coisa
Parece-me que em muitos desses exemplos podemos destacar uma lista limitada de tipos suportados, por isso std::variant
pode ser uma escolha melhor. Mas é claro que é difícil criar bibliotecas sem conhecer os produtos finais nos quais ela será usada. Você simplesmente não sabe quais tipos serão armazenados lá.A demonstração mostrou algumas coisas básicas, mas nas seções a seguir você aprenderá mais std::any
, portanto continue lendo.Criar std :: any
Existem várias maneiras de criar um objeto do tipo std::any
:- inicialização padrão - o objeto está vazio
- inicialização direta com valor / objeto
- indicando diretamente o tipo de objeto -
std::in_place_type
- através da
std::make_any
Por exemplo:
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");
Alterar valor
Há std::any
duas maneiras de alterar o valor atualmente armazenado : método emplace
ou atribuição: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 do objeto
A chave da segurança std::any
é a falta de vazamento de recursos. Para conseguir isso, ele std::any
destruirá qualquer objeto ativo antes de atribuir um novo valor.std::any var = std::make_any<MyType>();
var = 100.0f;
std::cout << std::any_cast<float>(var) << "\n";
Este código produzirá o seguinte:MyType::MyType
MyType::~MyType
100
O objeto é std::any
inicializado com um objeto do tipo MyType, mas antes de atribuir um novo valor (100.0f), o destruidor é chamado MyType
.Obtendo acesso a um valor
Na maioria dos casos, você tem apenas uma maneira de acessar o valor em std::any
- std::any_cast
, ele retorna os valores do tipo especificado se ele estiver armazenado no objeto.Esse recurso é muito útil, pois possui várias maneiras de usá-lo:- retornar uma cópia do valor e sair
std::bad_any_cast
por erro - retornar um link para o valor e sair
std::bad_any_cast
por erro - retornar um ponteiro para um valor (constante ou não) ou nullptr no caso de um erro
Veja um exemplo: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 você pode ver, temos duas maneiras de rastrear erros: através de exceções ( std::bad_any_cast
) ou retornando um ponteiro (ou nullptr
). A função std::any_cast
para retornar ponteiros está sobrecarregada e marcada como noexcept
.Desempenho e uso de memória
std::any
Parece uma ferramenta poderosa, e você provavelmente a usará para armazenar dados de diferentes tipos, mas qual é o preço?O principal problema é a alocação de memória extra.std::variant
e std::optional
não requer alocações de memória adicionais, mas isso ocorre porque os tipos de dados armazenados no objeto são conhecidos antecipadamente. std :: any não possui essas informações, portanto pode usar memória adicional.Isso vai acontecer sempre ou às vezes? Quais regras? Isso acontecerá com tipos simples como int?Vamos ver o que o padrão diz:As implementações devem evitar o uso de memória alocada dinamicamente para um pequeno valor contido. Exemplo: onde o objeto construído está mantendo apenas um int. Essa otimização de objetos pequenos deve ser aplicada apenas aos tipos T para os quais is_nothrow_move_constructible_v é verdadeiro
Uma implementação deve evitar o uso de memória dinâmica para dados armazenados de tamanho pequeno. Por exemplo, quando um objeto é criado, armazenando apenas int. Essa otimização para objetos pequenos deve ser aplicada apenas aos tipos T para os quais is_nothrow_move_constructible_v é verdadeiro.
Como resultado, eles propõem o uso de Small Buffer Optimization / SBO para implementações. Mas isso também tem um preço. Isso aumenta o tipo - para cobrir o buffer.Vejamos o tamanho std::any
, eis os resultados de vários compiladores:Em geral, como você pode ver, std::any
esse não é um tipo simples e gera custos adicionais. Geralmente, é preciso muita memória, devido ao SBO, de 16 a 32 bytes (no GCC ou clang ... ou até 64 bytes no MSVC!).Migrando do impulso :: any
boost::any
Foi introduzido em algum lugar em 2001 (versão 1.23.0). Além disso, o autor boost::any
(Kevlin Henney) também é o autor da proposta std::any
. Portanto, esses dois tipos estão intimamente relacionados, a versão do STL é fortemente baseada em seu antecessor.Aqui estão as principais mudanças:A principal diferença é que ele boost::any
não usa SBO, portanto, consome significativamente menos memória (no GCC8.1 seu tamanho é 8 bytes), mas, por causa disso, aloca dinamicamente a memória, mesmo para tipos pequenos como int.Exemplos de uso de std :: any
A principal vantagem std::any
é a flexibilidade. Nos exemplos abaixo, você pode ver algumas idéias (ou implementações específicas) em que usá- std::any
lo torna o aplicativo um pouco mais fácil.Análise de arquivo
Nos exemplos de std::variant
( você pode vê-los aqui [eng] ), você pode ver como você pode analisar os arquivos de configuração e armazenar o resultado em uma variável de tipo std::variant
. Agora você está escrevendo uma solução muito geral, talvez faça parte de alguma biblioteca, e talvez não esteja ciente de todos os tipos possíveis de tipos.O armazenamento de dados usando std::any
para parâmetros provavelmente será muito bom em termos de desempenho e, ao mesmo tempo, proporcionará a flexibilidade da solução.Mensagens
No Windows Api, que é principalmente escrito em C, existe um sistema de mensagens que usa o ID da mensagem com dois parâmetros opcionais que armazenam dados da mensagem. Com base nesse mecanismo, você pode implementar o WndProc, que processa a mensagem enviada para sua janela.LRESULT CALLBACK WindowProc(
_In_ HWND hwnd,
_In_ UINT uMsg,
_In_ WPARAM wParam,
_In_ LPARAM lParam
);
O fato é que os dados são armazenados em wParam
ou lParam
de diferentes formas. Às vezes, você só precisa usar alguns bytes wParam
.E se mudarmos esse sistema para que a mensagem possa passar alguma coisa para o método de processamento?Por exemplo: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 exemplo, você pode enviar uma mensagem para a janela:Message m(Message::Type::ShowWindow, std::make_pair(10, 11));
yourWindow.HandleMessage(m);
Uma janela pode responder a uma mensagem como esta: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;
}
}
Obviamente, você precisa determinar como o tipo de dados é armazenado nas mensagens, mas agora você pode usar tipos reais em vez de diferentes truques com números.Propriedades
O documento original que qualquer um representa para C ++ (N1939) mostra um exemplo de um objeto de propriedade:struct property
{
property();
property(const std::string &, const std::any &);
std::string name;
std::any value;
};
typedef std::vector<property> properties;
Este objeto parece muito útil porque pode armazenar muitos tipos diferentes. O primeiro que me vem à cabeça é um exemplo de uso em um gerenciador de interface com o usuário ou em um editor de jogos.Nós atravessamos as fronteiras
No r / cpp, houve um fluxo sobre std :: any. E havia pelo menos um ótimo comentário que resume quando um tipo deve ser usado.A partir deste comentário :A conclusão é que std :: any permite transferir direitos a dados arbitrários através de fronteiras que não sabem sobre seu tipo.
Tudo o que eu falei antes se aproxima dessa idéia:- na biblioteca da interface: você não sabe quais tipos o cliente deseja usar lá
- mensagens: a mesma idéia - dê flexibilidade ao cliente
- análise de arquivo: para suportar qualquer tipo
Total
Neste artigo, aprendemos muito sobre std::any
!Aqui estão algumas coisas para manter na mente:std::any
não é uma classe de modelostd::any
usa otimização de objetos pequenos, para que não aloque dinamicamente memória para tipos simples como int ou double, e para tipos maiores, será usada memória adicionalstd::any
pode ser chamado de "pesado", mas oferece segurança e maior flexibilidade- O acesso aos dados
std::any
pode ser obtido com a ajuda de any_cast
, que oferece vários "modos". Por exemplo, em caso de erro, ele pode gerar uma exceção ou simplesmente retornar nullptr - use-o quando não souber exatamente quais tipos de dados são possíveis; caso contrário, considere usar
std::variant