Tudo o que você precisa saber sobre std :: any

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 .

imagem

Com a ajuda de std::optionalvocê pode armazenar um tipo de tipo. Com a ajuda de std::variantvocê pode armazenar vários tipos em um objeto. E o C ++ 17 nos fornece outro tipo de invólucro - std::anyque 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::anydo 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;
//   :

//    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 produzirá:

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

O exemplo acima mostra algumas coisas importantes:

  • std::anystd::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::anye 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::anytotalmente 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::variantpode 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); // 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");

Alterar valor


std::anyduas maneiras de alterar o valor atualmente armazenado : método emplaceou 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::anydestruirá 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::anyinicializado 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_castpor 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); // 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 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_castpara retornar ponteiros está sobrecarregada e marcada como noexcept.

Desempenho e uso de memória


std::anyParece 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::optionalnã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:

Compiladorsizeof (qualquer)
GCC 8.1 (Coliru)dezesseis
Clang 7.0.0 (Wandbox)32.
MSVC 2017 15.7.0 32 bits40.
MSVC 2017 15.7.0 de 64 bits64

Em geral, como você pode ver, std::anyesse 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::anyFoi 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:

FunçãoBoost.Any
(1.67.0)
std :: any
Alocação de memória adicionalsimsim
Otimização de objetos pequenosnãosim
substituirnãosim
in_place_type_t no construtornãosim


A principal diferença é que ele boost::anynã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::anylo 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::anypara 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 wParamou lParamde 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 modelo
  • std::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 adicional
  • std::any pode ser chamado de "pesado", mas oferece segurança e maior flexibilidade
  • O acesso aos dados std::anypode 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

All Articles