Tout ce que vous devez savoir sur std :: any

Bonjour, Habr! Nous vous présentons une traduction de l'article «Tout ce que vous devez savoir sur std :: any de C ++ 17» de Bartlomiej Filipek .

image

Avec l'aide de std::optionalvous pouvez stocker un type de type. Avec l'aide de std::variantvous pouvez stocker plusieurs types dans un seul objet. Et C ++ 17 nous fournit un autre type de wrapper - std::anyqui peut stocker n'importe quoi tout en restant sûr pour le type.

Les bases


Auparavant, la norme C ++ ne fournissait pas de nombreuses solutions au problème de stockage de plusieurs types dans une variable. Bien sûr, vous pouvez l'utiliser void*, mais ce n'est pas du tout sûr.

Théoriquement, void*vous pouvez l'envelopper dans une classe où vous pouvez en quelque sorte stocker le type:

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

Comme vous pouvez le voir, nous avons obtenu une certaine forme de base std::any, mais pour assurer la sécurité du type, MyAnynous avons besoin de vérifications supplémentaires. C'est pourquoi il vaut mieux utiliser une option de la bibliothèque standard que de prendre votre décision.

Et c'est ce que c'est std::anyde C ++ 17. Il vous permet de stocker quoi que ce soit dans l'objet et signale une erreur (lève une exception) lorsque vous essayez d'accéder en spécifiant le mauvais type.

Petite démo:

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";
}

Ce code affichera:

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

L'exemple ci-dessus montre certaines choses importantes:

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

L'exemple ci-dessus semble impressionnant - une variable de type réel en C ++! Si vous aimez beaucoup JavaScript, vous pouvez même créer toutes vos variables de type std::anyet utiliser C ++ comme JavaScript :)

Mais peut-être y a-t-il des exemples d'utilisation normale?

Quand utiliser?


Bien qu'il soit void*perçu par moi comme une chose très dangereuse avec une gamme très limitée d'utilisations possibles, il est std::anycomplètement sûr pour le type, donc il a de bonnes façons de l'utiliser.

Par exemple:

  • Dans les bibliothèques - lorsque votre bibliothèque doit stocker ou transférer des données et que vous ne savez pas de quel type ces données peuvent être
  • Lors de l'analyse des fichiers - si vous ne pouvez vraiment pas déterminer quels types sont pris en charge
  • Messagerie
  • Interaction avec le langage de script
  • Création d'un interprète pour un langage de script
  • Interface utilisateur - Les champs peuvent stocker n'importe quoi

Il me semble que dans beaucoup de ces exemples, nous pouvons mettre en évidence une liste limitée de types pris en charge, il std::variantpeut donc être un meilleur choix. Mais bien sûr, il est difficile de créer des bibliothèques sans connaître les produits finaux dans lesquels elles seront utilisées. Vous ne savez tout simplement pas quels types y seront stockés.

La démonstration a montré quelques éléments de base, mais dans les sections suivantes, vous en apprendrez plus std::any, alors continuez à lire.

Créer std :: any


Il existe plusieurs façons de créer un objet de type std::any:

  • initialisation standard - l'objet est vide
  • initialisation directe avec valeur / objet
  • indiquant directement le type d'objet - std::in_place_type
  • via std::make_any

Par exemple:

//  :
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");

Changer la valeur


Il std::anyexiste deux façons de modifier la valeur actuellement stockée dans : méthode emplaceou affectation:

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);

Cycle de vie des objets


La clé de la sécurité std::anyest le manque de fuite de ressources. Pour ce faire, il std::anydétruira tout objet actif avant d'attribuer une nouvelle valeur.

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

Ce code affichera les éléments suivants:

MyType::MyType
MyType::~MyType
100

L'objet est std::anyinitialisé avec un objet de type MyType, mais avant d'attribuer une nouvelle valeur (100.0f), le destructeur est appelé MyType.

Accéder à une valeur


Dans la plupart des cas, vous n'avez qu'une seule façon d'accéder à la valeur dans std::any- std::any_cast, elle renvoie les valeurs du type spécifié si elle est stockée dans l'objet.

Cette fonctionnalité est très utile, car elle peut être utilisée de nombreuses façons:

  • retourner une copie de la valeur et quitter std::bad_any_casten cas d'erreur
  • retourner un lien vers la valeur et quitter std::bad_any_cast en cas d'erreur
  • retourne un pointeur sur une valeur (constante ou non) ou nullptr en cas d'erreur

Voir un exemple:

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();
    }
}

Comme vous pouvez le voir, nous avons deux façons de suivre les erreurs: via les exceptions ( std::bad_any_cast) ou en renvoyant un pointeur (ou nullptr). La fonction std::any_castde retour des pointeurs est surchargée et marquée comme noexcept.

Performances et utilisation de la mémoire


std::anyIl ressemble à un outil puissant, et vous l'utiliserez très probablement pour stocker des données de différents types, mais quel est le prix?

Le problème principal est l'allocation de mémoire supplémentaire.

std::variant et std::optionalne nécessite aucune allocation de mémoire supplémentaire, mais cela est dû au fait que les types de données stockées dans l'objet sont connus à l'avance. std :: any ne possède pas ces informations, il peut donc utiliser de la mémoire supplémentaire.

Cela se produira-t-il toujours ou parfois? Quelles règles? Cela se produira-t-il même avec des types simples comme int?

Voyons ce que dit la norme:
Les implémentations doivent éviter l'utilisation de mémoire allouée dynamiquement pour une petite valeur contenue. Exemple: où l'objet construit ne contient qu'un int. Cette optimisation de petit objet ne doit être appliquée qu'aux types T pour lesquels is_nothrow_move_constructible_v est vrai
Une implémentation doit éviter d'utiliser la mémoire dynamique pour les données stockées de petite taille. Par exemple, lorsqu'un objet est créé en stockant uniquement int. Cette optimisation pour les petits objets ne doit être appliquée qu'aux types T pour lesquels is_nothrow_move_constructible_v est vrai.

En conséquence, ils proposent d'utiliser Small Buffer Optimization / SBO pour les implémentations. Mais cela a aussi un prix. Cela rend le type plus grand - pour couvrir le tampon.

Regardons la taille std::any, voici les résultats de plusieurs compilateurs:

Compilateursizeof (any)
GCC 8.1 (Coliru)seize
Clang 7.0.0 (Wandbox)32
MSVC 2017 15.7.0 32 bits40
MSVC 2017 15.7.0 64 bits64

En général, comme vous pouvez le voir, std::anyce n'est pas un type simple et cela entraîne des coûts supplémentaires. Il prend généralement beaucoup de mémoire, en raison de SBO, de 16 à 32 octets (en GCC ou clang ... ou même 64 octets en MSVC!).

Migration depuis boost :: any


boost::anyIl a été introduit quelque part en 2001 (version 1.23.0). En outre, l'auteur boost::any(Kevlin Henney) est également l'auteur de la proposition std::any. Par conséquent, ces deux types sont étroitement liés, la version de STL est fortement basée sur son prédécesseur.

Voici les principaux changements:

Une fonctionBoost.Any
(1.67.0)
std :: any
Allocation de mémoire supplémentaireOuiOui
Optimisation des petits objetsnonOui
emplacenonOui
in_place_type_t dans le constructeurnonOui


La principale différence est qu'il boost::anyn'utilise pas SBO, donc il prend beaucoup moins de mémoire (dans GCC8.1 sa taille est de 8 octets), mais à cause de cela, il alloue dynamiquement de la mémoire même pour des petits types comme int.

Exemples d'utilisation de std :: any


Le principal avantage std::anyest la flexibilité. Dans les exemples ci-dessous, vous pouvez voir quelques idées (ou implémentations spécifiques) où son utilisation std::anyrend l'application un peu plus facile.

Analyse de fichiers


Dans les exemples de std::variant ( vous pouvez les voir ici [eng] ), vous pouvez voir comment analyser les fichiers de configuration et stocker le résultat dans une variable de type std::variant. Maintenant que vous écrivez une solution très générale, peut-être qu'elle fait partie d'une bibliothèque, alors vous ne connaissez peut-être pas tous les types de types possibles.

Le stockage des données à l'aide std::anyde paramètres est susceptible d'être assez bon en termes de performances, tout en vous offrant la flexibilité de la solution.

Messagerie


Dans Windows Api, qui est principalement écrit en C, il existe un système de messagerie qui utilise l'ID de message avec deux paramètres facultatifs qui stockent les données de message. Sur la base de ce mécanisme, vous pouvez implémenter WndProc, qui traite le message envoyé à votre fenêtre.

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

Le fait est que les données sont stockées sous wParamou lParamsous différentes formes. Parfois, vous n'avez besoin que de quelques octets wParam.

Que se passe-t-il si nous modifions ce système afin que le message puisse transmettre quoi que ce soit à la méthode de traitement?

Par exemple:

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;
};

Par exemple, vous pouvez envoyer un message à la fenêtre:

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

Une fenêtre peut répondre à un message comme celui-ci:

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;
    }
}

Bien sûr, vous devez déterminer comment le type de données est stocké dans les messages, mais maintenant vous pouvez utiliser des types réels au lieu de différentes astuces avec des nombres.

Propriétés


Le document d'origine que tout représente pour C ++ (N1939) montre un exemple d'un objet de propriété:

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

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

typedef std::vector<property> properties;

Cet objet semble très utile car il peut stocker de nombreux types différents. Le premier qui me vient à l'esprit est un exemple d'utilisation dans un gestionnaire d'interface utilisateur ou dans un éditeur de jeu.

Nous traversons les frontières


Dans r / cpp, il y avait un flux sur std :: any. Et il y avait au moins un excellent commentaire qui résume quand un type doit être utilisé.

De ce commentaire :
L'essentiel est que std :: any vous permet de transférer des droits sur des données arbitraires à travers les frontières qui ne connaissent pas son type.
Tout ce dont j'ai parlé auparavant est proche de cette idée:

  • dans la bibliothèque de l'interface: vous ne savez pas quels types le client veut y utiliser
  • messagerie: la même idée - donner au client la flexibilité
  • analyse de fichiers: pour prendre en charge tout type

Total


Dans cet article, nous avons beaucoup appris std::any!

Voici quelques éléments à garder à l'esprit:

  • std::any pas une classe modèle
  • std::any utilise l'optimisation des petits objets, donc il n'allouera pas dynamiquement de la mémoire pour les types simples tels que int ou double, et pour les types plus grands, une mémoire supplémentaire sera utilisée
  • std::any peut être appelé "lourd", mais il offre une sécurité et une plus grande flexibilité
  • L'accès aux données std::anypeut être obtenu à l'aide de any_cast, qui propose plusieurs "modes". Par exemple, en cas d'erreur, il peut lever une exception ou simplement retourner nullptr
  • utilisez-le lorsque vous ne savez pas exactement quels types de données sont possibles, sinon envisagez d'utiliser std::variant

All Articles