URL: https://linuxfr.org/news/cpp17-libere-size-data-et-empty Title: C++17 libère size(), data() et empty() Authors: lmg HS David Marec, Benoît Sibaud, Oliver, Davy Defaud, palm123, gbdivers, whity, claudex, audionuma et kp Date: 2016年12月12日T01:27:16+01:00 License: CC By-SA Tags: c++ et c++17 Score: 36 Cette évolution est dans la continuité de la libération de `begin()` et `end()` en C++11. Ces trois nouvelles fonctions, `size()`, `data()` et `empty()`, avaient été oubliées lors des deux dernières évolutions. Le C++17 corrige le tir. Tout d’abord, nous allons revenir sur les raisons de cette évolution. Puis, nous détaillerons ce qu’apporte chacune de ces trois fonctions. Et, pour conclure, nous verrons comment on va pouvoir les utiliser. [![Les fonctions empty(), data() et size() sortent de la prison « class » et rejoignent les fonctions begin() et end() qui les accueillent. Ces deux dernières fonctions accueillent également string_view. Derrières les barreaux de la prison des spécifications techniques, Unified-Call-Syntax va devoir y rester trois ans pour tenter de sortir avec C++20](https://cpp-frug.github.io/materials/images/fonctions_libres.svg)](https://github.com/cpp-frug/materials/blob/gh-pages/images/fonctions_libres.svg) ---- ---- # Fonction membre ou fonction libre, `dog.kick()` ou `kick(dog)` ? Fonction membre | Fonction libre --------------------|-------------------- `objet.fonction()` | `fonction(objet)` `dog.kick()` | `kick(dog)` `jean.nom()` | `nom(jean)` `conteneur.size()` | `size(conteneur)` `chien.courir()` | `courir(chien)` `chat.attrape(souris)`| `attrape(chat,souris)` `objet.service()` | `service(objet)` Cette question de notation assez anodine a des répercussions non anodines, elles, sur ce qu’il nous est permis de faire. D’un côté nous sommes dans la continuité du choix de la notation d’envoi de messages choisie par Smalltalk et, de fait, nous tendons à penser que nous faisons de l’orienté objet. De l’autre nous avons l’impression de faire du C ou du Pascal. Et pourtant, ce n’est guère différent (en termes d’ordre) de la notation d’appel employée en CLOS, dialecte orienté objet du LISP. En effet, dans ce dernier, nous aurions employé `(f x y)`. Dans l’article _Comment prendre en charge les multi‐methods en C++_, Jean‐Louis Leroy [discute justement de la perception de ces choix de notation](https://www.codeproject.com/articles/635264/open-multi-methods-for-cplusplus11-part-1#kick_dog). Il est difficile de ne pas abonder dans son sens sur deux autres points. D’abord, prononcé à voix haute « _kick dog_ » passe mieux que « _dog kick_ ». Et ensuite, et surtout, qui est véritablement l’**acteur** ? * le chien, qui va alors aboyer et renoncer, parce que c’est dans sa nature ? * un tiers, qui est l’auteur du coup de pied ? Mais, mettons de côté ces aspects philosophiques, et recentrons‐nous sur les répercussions plus techniques et objectives. Mettons de côté aussi les aspects de [_dispatch‐multiple_](https://en.wikipedia.org/wiki/Multiple_dispatch) (désolé, la page en français est un peu pauvre sur le sujet) où une symétrie fait que `x.f(y)` n’a certainement pas plus de légitimité que `y.f(x)` et de fait que `f(x, y)`. Ce n’est pas le point de cette évolution du standard. Alors qu’Alexander Stepanov préparait avec Lee Meng [une bibliothèque _template_ générique dès le début des années 90](https://www.sgi.com/tech/stl/drdobbs-interview.html), et qu’ils la peaufinaient pour satisfaire les exigences du comité de standardisation du C++, ils avaient dû faire des concessions pour qu’elle soit validée par ce que nous pourrions qualifier d’_intelligentsia OO_. Stepanov se défend d’avoir conçu cette bibliothèque en suivant le paradigme objet et évoque dans son [_Notes on Programming_](http://stepanovpapers.com/notes.pdf) sa concession au sujet de `size()` qu’il aurait préférée libre. Ainsi, plutôt que d’employer `size(v)`, nous employons depuis `v.size()`. Cette bibliothèque, vous en avez probablement entendu parler, c’est la [STL](http://www.sgi.com/tech/stl/ "Standard Template Library"), qui fut incorporée dans la bibliothèque standard du C++ en 98.> « _While we could make a member function to return length, it is better to make it a global friend function. If we do that, we will be able eventually to define the same function to work on built‐in arrays and achieve greater uniformity of design. I made size into a member function in STL in an attempt to please the standard committee. I knew that begin, end and size should be global functions but was not willing to risk another fight with the committee. In general, there were many compromises of which I am ashamed. It would have been harder to succeed without making them, but I still get a metallic taste in my mouth when I encounter all the things that I did wrong while knowing full how to do them right. Success, after all, is much overrated. I will be pointing to the incorrect designs in STL here and there: some were done because of political considerations, but many were mistakes caused by my inability to discern general principles._ » Traduction :> « S’il est possible de créer une fonction membre pour retourner la taille [de `vector`], il est préférable d’en faire une fonction générale. Ce faisant, nous pourrons par la suite utiliser cette même fonction sur les tableaux de base et obtenir ainsi une conception beaucoup plus uniforme. J’ai fait de `size` une fonction membre dans la STL pour plaire au comité de standardisation. Je savais que `begin`, `end` et `size` auraient dû être des fonctions génériques, mais je ne souhaitais pas entrer à nouveau en conflit avec le comité. Globalement, je ne suis pas très fier de nombre de compromis. Sans ceux‐ci, il aurait été plus difficile de réussir, mais j’ai toujours un arrière‐goût amer lorsque je tombe sur tous ces trucs que j’ai mal faits alors que je savais parfaitement comment les concevoir correctement. Finalement, ce succès est pas mal surestimé. Je pointerai les conceptions erronées dans la STL ici et là : si certaines sont dues à des considérations politiques, la plupart étaient des erreurs provoquées par mon incapacité à dégager de grands principes. » Des années plus tard, dans ses GOTW compilés dans _Exceptional C++_, Herb Sutter nous fait réfléchir à l’[interface monolithique de `std::string`](http://www.gotw.ca/gotw/084.htm) et à toutes ses fonctions qui auraient pu être libres plutôt que membres dans cette classe standard — `std::string` représente des chaînes de caractères. Sa préconisation est d’élargir au maximum l’interface des classes par l’extérieur et de garder membres uniquement les fonctions qui ont besoin d’aller taper dans les membres privés, ou qui peuvent être supplantées par redéfinition ([_overridding_](https://en.wikipedia.org/wiki/Method_overriding) en VO). De ce côté‐là, `std::string` est clairement un contrevenant à la règle. Pire, qui n’a jamais croisé des classes de chaînes définies juste parce qu’il manquait des petites choses à `std::string`, comme par exemple : `std::string::trim_right()`, `std::string::toupper()`, `std::string::split()`, etc. ? Où est le mal à cela ? Si nous définissons `My::String::trim_left()`, nous avons ce que nous voulons, non ? Et si nous voulons l’appliquer sur une `std::string` ou un `char const*`, nous avons juste à construire un objet `My::String` à partir de ces derniers. Quoi ?! Il faut encore gérer un autre type `foss::string` ? Bah, nous convertissons d’abord vers un `char const*` ou vers une `std::string`, puis vers une `My::String`. Non ? Il se trouve que, justement, C++17 nous apporte [`std::string_view`](http://en.cppreference.com/w/cpp/experimental/basic_string_view), une classe qui veut les unifier toutes, et qui garantit une efficacité équivalente au couple pointeur + taille quand nous manipulons des tableaux en C -- et d’[autres classes pourraient suivre](https://github.com/isocpp/CppCoreGuidelines/blob/master/CppCoreGuidelines.md#faq55-if-youre-using-the-standard-types-where-available-why-is-the-gsl-string_span-different-from-the-string_view-in-the-library-fundamentals-1-technical-specification-and-c17-working-paper-why-not-just-use-the-committee-approved-string_view) dans cette thématique. C’est juste une _vue_ sur une chaîne valide, un machin qui permet de regarder sans toucher, sans posséder et sans allouer ! Malheureusement, vu que `split()` aurait été définie membre d’une classe tierce, nous ne pourrions pas l’appliquer directement sur la `string_view`. Techniquement, de quoi a‐t‐on besoin pour définir `split()` ? D’un moyen de tester chaque caractère ou, mieux, d’une fonction (libre de préférence) qui permette de chercher un index dans une chaîne et aussi de pouvoir définir des sous‐chaînes. Quel rapport avec `std::size()` & Cie ? Nous sommes dans la même problématique. Nous voudrions pouvoir écrire des algorithmes génériques sans avoir besoin de savoir ce que nous manipulons, et sans avoir besoin de payer une conversion chère vers un type bidon. Et si la chose que nous manipulons n’a pas le service, nous le lui rajoutons par l’extérieur. Étendre par l’extérieur est ce qui nous ouvre le plus de possibilités. De plus, nous restons dans le credo C++ de ne pas avoir à payer pour ce dont on n’a pas besoin. C’est ce sur ce sujet que [Herb Sutter et Bjarne Stroustrup](https://isocpp.org/blog/2016/02/a-bit-of-background-for-the-unified-call-proposal) se sont entretenus début 2014 : proposer une unification des appels de fonctions, [_Unified Call Syntax_](http://www.open-std.org/jtc1/sc22/wg21/docs/papers/2014/n4165.pdf), entre `f(x,y)` et `x.f(y)`. Si, pour maintenir une certaine compatibilité, ils ont convenu que ces deux écritures _pouvaient_ définir des comportements différents, ils se sont entendus pour définir qu’en cas d’échec d’une écriture, le compilateur testerait l’autre. Il s’en est suivi une levée de boucliers et, finalement, l’idée des UCS a été bottée en touche. Pour l’instant ? # Détaillons ces trois nouvelles fonctions du C++17 `std::empty()` -------------- C’est la plus simple du lot. Elle indique si son paramètre est vide. Elle s’applique à n’importe quoi qui puisse être vide. Par défaut, elle sait s’appliquer à des tableaux statiques C (elle renvoie toujours faux dans ce cas), à n’importe quoi qui dispose d’une fonction membre `empty()`, ou encore aux `std::initializer_list`. `std::size()` ------------- Cette fonction est beaucoup plus intéressante. Savez‐vous comment obtenir le nombre d’éléments dans un tableau statique en C ? Si vous connaissez la réponse, vous connaissez aussi probablement sa limitation : ```C #define size(T) (sizeof(T)/sizeof(T[0])) int const t[] = {1, 2, 3}; void f() { printf("f: %zu\n", size(t)); } void g(int const t[3]) { printf("g: %zu\n", size(t)); } int main(){ f(); g(t); } ``` Une compilation en 64 bits ([modèle LP64 ou LLP64](https://en.wikipedia.org/wiki/64-bit_computing)) donnera : ``` f: 3 g: 2 ``` Eh oui, `g()` reçoit non pas un tableau, mais un pointeur ! Cela fait toute la différence. Autant dire que cette approche nous trahit facilement. Fort heureusement, nous sommes en C++, et dans ce dernier on peut forcer `g()` à attendre un tableau en recevant une référence sur un tableau : ```C++ void g(int (&t)[3]); ... int t3[] = {1, 2, 3}; int t2[] = {1, 2}; g(t3); // compile correctement g(t2); // est rejeté par le compilateur ``` Là, vous me direz, « la taille est écrite en dur dans le type du paramètre formel, ça ne sert à rien ! ». OK, « templatisons » et enchaînons pour obtenir directement la taille d’un tableau. ```C++ template std::size_t size(int (&t)[N]) { return N; } ``` Et le truc qui serait cool, c’est : ```C++ struct nombres { enum type { un, deux, trois, MAX__ }; char const* chaines[] = { "un", "deux", "trois", "quatre" }; } static_assert(nombres::MAX__ == size(nombres::chaines)); ``` Sauf que cela ne compilera pas parce que cette fonction `size()` n’est pas `constexpr`. En C++11, c’est facile à rajouter, et pourtant officiellement, c’est de `std::extent::value`, seulement, dont nous disposons. En C++17, ça y est, nous avons `std::size()`. C’est explicite, c’est simple à utiliser, et cela fait ce dont nous avions besoin depuis longtemps. Note : en C++98, nous devions recourir à boost ou à Loki pour avoir un équivalent au `static_assert` du C++11, et à d’autres feintes telles que la suivante pour avoir une taille exploitable dans une expression constante : ```C++ namespace { template char (&array_size_helper(T (&)[N]))[N]; } // namespace #define array_size(array) (sizeof ::array_size_helper(array)) ``` `std::data()` ------------- Ici, rien de bien compliqué de prime abord : nous avons une fonction qui nous renvoie l’adresse où est stocké le premier élément d’une séquence d’éléments contigus en mémoire. De même, cela est défini automatiquement sur tout conteneur qui expose une fonction membre éponyme, sur les tableaux statiques à la C, et sur les `std::initializer_list`. Nous pouvons nous poser la question de l’intérêt, car il suffisait d’écrire : ```C++ template void dump(Collection const& c) { api_c_(&c[0], &[size(c)]); } ``` Sauf que... Et si l’opérateur d’accès indexé sur la collection spécifiait dans ses préconditions que l’index devait être strictement inférieur à la taille ? ```C++ reference operator[](size_t index) [[expects: index < size()]] // syntaxe probable -- C++20 { assert(index < this->size()); // syntaxe 98-17 return quivabien(index); } ``` Bon, pour le second paramètre réel passé à `api_c`, on peut écrire `&c[0]+size(c)`. Seulement, si la collection est vide, nous sommes de nouveaux grillés. Et encore, c’est à supposer que la fonction avec une API C n’exige pas que le premier pointeur pointe bien vers un élément déréférençable, même si le second pointeur est égal au premier — ceci est le cas de beaucoup de fonctions du standard C : un `memcpy()` de 0 élément vers ou depuis `NULL` est officiellement illégal, et donc un [UB](https://en.wikipedia.org/wiki/Undefined_behavior "Undefined behavior"). Bref, vous l’aurez compris, `data()` ne s’encombre pas avec ces considérations. Et depuis sa libération elle est donc compatible avec les tableaux statiques. Bonjour `data(c)`. Adieu `&c[0]`. # Et pour s’en servir ? C’est bien beau tout ça, mais nous n’avons pas abordé la façon dont nous pouvons nous en servir. Vous devez vous dire que nous sommes un peu bêtes. Il suffit d’écrire, pour reprendre le dernier exemple : ```C++ template void dump(Collection const& c) { api_c_(std::data(c), std::data(c)+std::size(c)); // Ou si on suppose que les préconditions de l'API C soient exagérées if (!std::empty(c)) api_c_(std::data(c), std::data(c)+std::size(c)); } ``` Et voilà ! Non ? Pouvons‐nous nous en servir sur un type `blas::vector` qui expose `data`, `empty` et `size` en membres ? Oui, c’est prévu pour ! C’est donc parfait. Mais... Et si ces fonctions n’étaient pas membres mais libres. Ou pire : et si elles avaient une version membre dont le nom commençait par une majuscule ? Ah. Il n’y aurait qu’à définir une surcharge de `std::empty(blas::vector const& v)` qui renvoie `v.Empty()`. Le hic est que, bien que la norme nous autorise à procéder ainsi dans ce cas de figure précis (_i.e._ rajouter une spécialisation pour un type non standard dans l’espace de noms `std`), il est fortement encouragé de procéder autrement. On préfère ignorer ainsi les exceptions à l’interdiction de rajouter des définitions dans `std::`. Il faut bien reconnaître qu’il est plus simple de retenir une règle générale que d’expliquer les exceptions autorisées. > _[C++11: 17.6.4.2.1/1]: The behavior of a C++ program is undefined if it adds declarations or definitions to namespace std or to a namespace within namespace std unless otherwise specified. A program may add a template specialization for any standard library template to namespace std only if the declaration depends on a user‐defined type and the specialization meets the standard library requirements for the original template and is not explicitly prohibited._ Traduction :> Le comportement d’un programme C++ est indéfini s’il ajoute des déclarations ou définitions à l’espace de noms std ou à un espace de noms au sein de std, sauf indication contraire. Un programme pourra ajouter une spécialisation pour tout modèle de l’espace de noms std si et seulement si, cette déclaration concerne un type défini par l’utilisateur, que cette spécialisation convient aux exigences du modèle de base et qu’elle n’est pas explicitement interdite. Procédons autrement donc. Si seulement nous pouvions nous contenter d’un `empty(v)` sans avoir besoin de taper ces fichus noms d’espaces de noms (sic), ça serait parfait. Il se trouve que... justement, si ! Nous pouvons le faire. Si l’on écrit : ```C++ template void dump(Collection const& c) { if (!empty(c)) api_c_(data(c), data(c)+size(c)); } ``` Cela va compiler avec n’importe quel type défini par l’utilisateur qui dispose de fonctions `empty()`, `data()` et `size()` qui sont définies dans le même espace de noms que celui où ce type utilisateur est défini. On appelle ce principe le _Koenig Lookup_ (comme dans [Andrew Koenig](http://www.drdobbs.com/cpp/a-personal-note-about-argument-dependent/232901443)) ou encore plus généralement aujourd’hui l’[_Argument‐Dependent (name) Lookup_](http://en.cppreference.com/w/cpp/language/adl), §3.4.2 de la norme. Il ne reste que les cas des tableaux statiques à la C. Ce ne sont pas des types définis dans un espace de noms. L’ADL ne peut pas être appliqué. Ainsi, pour eux, on a besoin de rajouter ces trois lignes au début de la fonction : ```C++ using std::empty; using std::data; using std::size; ``` Pour `blas::vector`, il ne nous reste qu’à définir ces trois fonctions dans l’espace de noms `blas`. Si maintenant cette bibliothèque nous interdit aussi de définir quoi que ce soit dans son espace de noms, alors il n’y a plus qu’à ouvrir une fiche d’anomalie sur le système de suivi de bogues en expliquant qu’il serait vachement sympa d’ouvrir la compatibilité avec le C++, avec explications sur l’ADL/KNL à l’appui. Si maintenant vous deviez écrire une bibliothèque d’une qualité et d’une robustesse à toute épreuve, vous êtes invités à lire ce billet fort instructif d’Eric Niebler, qui est au cœur de la STL v2, au sujet du [design de points de customisation](http://ericniebler.com/2014/10/21/customization-point-design-in-c11-and-beyond/?ModPagespeed=noscript).

AltStyle によって変換されたページ (->オリジナル) /