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 :
1 2 3 4 5 |
struct any { void* data; id type_identifier; } |
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é :
1 2 3 4 5 6 7 8 9 10 |
struct property { property(); property(const std::string &, const std::any &); std::string name; std::any value; }; typedef std::list<property> properties; |
Manipuler std::any
La création d’un std::any est possible de différentes manières :
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
// initialisation par défaut (le any est vide). std::any a1; // initialisation avec une valeur. std::any a2 = 34; // a2 contient un int. std::any a3 = QString("Salut"); // a3 contient une QString. // copie. a1 = a2; std::any a4 = a3; // initialisation 'in_place'. std::any a5{std::in_place_type<std::string>, "Bonjour !"}; // fabrique make_any. std::any a6 = std::make_any<std::string>("Bonjour !"); |
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 :
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 |
struct Foo { Foo() { std::cout << "default constructor.\n"; } Foo(const Foo&) { std::cout << "copy constructor.\n"; } Foo& operator=(const Foo&) { std::cout << "copy assignment.\n"; return *this; } Foo(Foo&&) { std::cout << "move constructor.\n"; } Foo& operator=(Foo&&) { std::cout << "move assignment.\n"; return *this;} ~Foo() noexcept { std::cout << "destructor.\n"; } }; int main() { std::cout << "1\n"; std::any a = std::make_any<Foo>(); std::cout << "2\n"; a = 25; std::cout << "3\n"; std::any a2 = a; std::cout << "4\n"; a = Foo{}; std::cout << "5\n"; } |
(tester sur coliru)
1 2 3 4 5 6 7 8 9 10 11 |
1 default constructor. 2 destructor. 3 4 default constructor. move constructor. destructor. 5 destructor. |
La fonction template emplace permet aussi de remplacer l’objet courant en faisant une construction in-place :
1 2 |
a.emplace<int>(12); a.emplace<std::string>("hello"); |
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 :
1 2 3 4 5 6 |
std::any a; std::cout << std::boolalpha << a.has_value() << '\n'; a = 12; std::cout << a.has_value() << '\n'; a.reset(); std::cout << a.has_value() << '\n'; |
1 2 3 |
false true false |
std::any offre aussi la fonction membre type qui retourne le std::type_info du type de l’objet contenu :
1 2 3 4 5 6 |
std::any a; std::cout << a.type().name() << '\n'; a = 25; std::cout << a.type().name() << '\n'; a = "hello"s; std::cout << a.type().name() << '\n'; |
1 2 3 |
v i NSt7__cxx1112basic_stringIcSt11char_traitsIcESaIcEEE |
Vous pouvez remarquer que type() retourne void dans le cas où le any ne contient pas d’élément :
1 2 |
std::any a; std::cout << std::boolalpha << (a.type() == typeid(void)) << '\n'; |
1 |
true |
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é :
1 2 3 4 5 6 7 8 9 |
std::any a{ "hello"s }; auto s = std::any_cast<std::string>(a); try { [[maybe_unused]] auto i = std::any_cast<long unsigned>(a); } catch (std::bad_any_cast& e) { std::cout << e.what() << std::endl; // GCC 8.1, stdout: bad any_cast } |
Notez qu’il est également possible de récupérer une référence ou un pointeur sur l’objet en question :
1 2 3 4 5 6 7 |
std::any a{ "hello"s }; auto s = std::any_cast<std::string>(a); auto& ref = std::any_cast<const std::string&>(a); auto&& rvref = std::any_cast<std::string&&>(std::move(a)); auto ptr = std::any_cast<std::string>(&a); |
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 :
1 2 3 4 |
if(auto object = std::any_cast<MyType>(&myAny)) { std::cout << *object << '\n'; } |
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<T> 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 :
Compilateur | sizeof(any) |
---|---|
GCC 8.1(Coliru) | 16 |
Clang 7.0 (Wandbox) | 32 |
MSVC 15 32bit | 40 |
MSVC 15 64bit | 64 |
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
- [EN] cppreference.com – std::any
- [EN] cppreference.com – std::make_any
- [EN] cppreference.com – std::any_cast
- [EN] cppreference.com – std::bad_any_cast
- [EN] open-std.org | n4480 – std::any
- [EN] open-std.org | n3804 “Any Library Proposal”
- [EN] open-std.org | n1939 “Any Library Proposal for TR2”
- h-deb | std::any
- [EN] bfilipek.com | std::any
- [EN] msdn.com | Casey Carter | std::any, How, when and why ?
- [EN] reddit.com | Why std::any was added to C++17 ?
You must log in to post a comment.