L’arrivée du C++17 – std::any

Bonjour à tous ! L’article d’aujourd’hui traite d’un nouveau venu dans la bibliothèque standard avec C++17 : std::any.

Introduction à std::any

std::any est un conteneur qui permet de stocker une valeur de n’importe quel type copiable avec un typage sécurisé.

Notez que any n’est pas un type paramétré, puisqu’il peut potentiellement accepter tout type CopyConstructible. Basiquement,  any est un wrapper proposant une interface type-safe pour la manipulation d’un void* couplé à un identifiant de type :

std::any est destiné aux (rares) cas où on a absolument aucune idée du type qui va être manipulé et est basé sur boost::any. La n1939 (première proposition datant de 2006) présentait parmi ses exemples l’implémentation d’une propriété :

Manipuler std::any

La création d’un std::any est possible de différentes manières :

La construction “in place” est peut-être inédite pour beaucoup d’entre vous puisqu’elle a été introduite avec C++17 pour les types std::variant, std::any et std::optional de manière à indiquer que le type doit être construit in-place, c’est-à-dire sans copie, directement à partir des arguments fournis. Ces trois types proposent des surcharges de constructeurs pour la construction in-place. Je vous renvois à cette page de cppreference pour plus de détails.

std::make_any est une fabrique permettant de simplifier la syntaxe de la construction in-place. Cette fonction libre renvoie simplement std::any(std::in_place_t<T>, std::forward<Args>(args)...); .

Vous noterez qu’à tout moment il est possible de modifier de changer l’objet contenu dans le std::any à l’aide de l’opérateur d’affectation. Puisque std::any gère la durée de vie de l’objet pour nous, l’objet sera détruit à chaque affectation :

(tester sur coliru)

La fonction template  emplace permet aussi de remplacer l’objet courant en faisant une construction in-place :

std::any::emplace retourne l’objet créé ( std::decay_t<T>  pour être exact).

La fonction membre std::any::has_value permet de tester si il existe un objet contenu tandis que std::any::reset détruit l’objet courant :

std::any offre aussi la fonction membre type qui retourne le std::type_info du type de l’objet contenu :

Vous pouvez remarquer que type() retourne void dans le cas où le any ne contient pas d’élément :

Récupérer la valeur : std::any_cast

Comment récupérer le contenu d’un std::any me direz vous ? Et bien   std::any_cast répond précisément à cet objectif.

Cette fonction libre template tente de retourner l’objet contenu en fonction du type spécifié et lève std::bad_any_cast si l’objet n’est n’est pas constructible en le type demandé :

Notez qu’il est également possible de récupérer une référence ou un pointeur sur l’objet en question :

La version retournant un pointeur est un cas particulier puisqu’elle ne lève pas d’exception en cas d’erreur. Si vous souhaitez éviter les exceptions, cette version retournera nullptr en cas de cast invalide :

Performance et overhead

std::any a recours à l’allocation dynamique pour créer l’objet contenu. En effet, le type fourni pouvant être de taille totalement variable, il n’est pas possible de réserver un espace mémoire dans lequel tous les types pourraient tenir (ce que fait  std::variant par exemple).

Cela étant,  std::any peut profiter de la Small Buffer Optimisation (SBO). L’idée est exactement celle de la Small String Optimisation utilisée pour  std::string : si le type est suffisamment petit, il sera directement stocké dans l’objet  std::any à la place du pointeur vers la zone mémoire allouée. Le standard ne spécifie pas la taille d’un  std::any qui est laissée à l’implémentation :

Implementations should avoid the use of dynamically allocated memory for a small contained value. Example: where the object constructed is holding only an int. Such small-object optimization shall only be applied to types  T for which  is_nothrow_move_constructible_v&lt;T&gt; is  true.

La taille de  std::any varie en fonction du compilateur, ci suit les mesures sur les compilateurs majeurs, effectuées par Bartek sur son blog :

Compilateursizeof(any)
GCC 8.1(Coliru)16
Clang 7.0 (Wandbox)32
MSVC 15 32bit40
MSVC 15 64bit64

On constate que Microsoft a ici fait le choix de fournir un buffer plus grand de manière à supporter des types de taille plus importantes sur la stack. En contrepartie, la consommation mémoire est élevée pour des types de petite taille.

Je précise que sizeof  retourne un nombre de bytes, pas de bits. MSVC 64bit utilise donc un  std::any de 64 bytes (soit 512 bits en général), ce qui semble assez énorme.

std::any apporte donc un surcoût mémoire assez conséquent lorsqu’on souhaite l’utiliser pour des types de taille réduite.

Cas d’usage de std::any

Mais alors, quand et pourquoi utiliser  std::any ?

L’usage de  std::any reste réservé à des cas relativement rares où on a réellement aucune idée de l’ensemble des types qui seront nécessaires.

En début d’article, je vous ai présenté l’exemple d’implémentation d’une propriété. D’une manière générale, les API avec des paramètres callback définis par l’utilisateur se prêtent bien à l’utilisation de  std::any, comme remplacement de  void* ou de  std::shared_ptr<void>.

Par rapport à void*, any propose une interface typesafe, une gestion de la mémoire et permet de profiter de la SBO, au prix d’un type plus lourd en mémoire. any permet également de connaitre le type contenu via la fonction type, et autorise la copie contrairement à std::shared_ptr<void>.

Pour ceux qui se demandent si il est intéressant de passer de boost::any à  std::any, la SBO est un avantage non négligeable et certaines fonctionnalités (telles que la construction in-place et la fonction emplace) ne sont pas disponibles avec  boost::any.

Si vous savez quels sont les types qui seront stockés, préférez  std::variant qui est destiné précisément à ce genre de cas. Je ferais d’ailleurs prochainement un article au sujet de  std::variant. En attendant, je vous remercie pour votre lecture et vous dit à une prochaine fois !

Références et ressources

%d bloggers like this: