Nom de l'implémentation et nom du résultat


Je voulais écrire ce billet en juillet, mais je ne pouvais pas, oh ironie , décider comment l'appeler. Les bons termes ne me sont venus à l'esprit qu'après la conférence de Kate Gregory à CppCon , et maintenant je peux enfin vous dire comment appeler les fonctions.


Bien sûr, il y a des noms qui ne portent pas du tout d'informations, comme int f(int x) . Ils n'ont pas non plus besoin d'être utilisés, mais ce n'est pas à propos d'eux. Il arrive parfois qu'il semble que les informations contenues dans le titre soient complètes, mais cela ne présente aucun avantage.


Exemple 1: std :: log2p1 ()


En C ++ 20, plusieurs nouvelles fonctions pour les opérations sur les bits ont été ajoutées à l'en-tête, entre autres std::log2p1 . Cela ressemble à ceci:


 int log2p1(int i) { if (i == 0) return 0; else return 1 + int(std::log2(x)); } 

Autrement dit, pour tout nombre naturel, la fonction renvoie son logarithme binaire plus 1, et pour 0, elle renvoie 0. Et ce n'est pas une tâche d'école pour l'opérateur if / else, c'est vraiment une chose utile - le nombre minimum de bits dans lequel cette valeur va tenir. Le deviner par le nom de la fonction est presque impossible.


Exemple 2: std :: bless ()


Maintenant, ce ne sera plus le nom


Une petite digression: en C ++, l'arithmétique des pointeurs ne fonctionne qu'avec des pointeurs vers des éléments de tableau. Ce qui, en principe, est logique: dans le cas général, l'ensemble des objets voisins est inconnu et «tout peut arriver en dix octets à droite de la variable i ». Il s'agit d'un comportement sans ambiguïté vague.


 int obj = 0; int* ptr = &obj; ++ptr; //   

Mais une telle restriction déclare une énorme quantité de comportement indéfini de code existant. Par exemple, voici une implémentation simplifiée de std::vector<T>::reserve() :


 void reserve(std::size_t n) { //      auto new_memory = (T*) ::operator new(n * sizeof(T)); //    … //   auto size = this->size(); begin_ = new_memory; //   end_ = new_memory + size; //     end_capacity_ = new_memory + n; //    } 

Nous avons alloué de la mémoire, déplacé tous les objets et essayons maintenant de nous assurer que les pointeurs indiquent où aller. Voici que les trois dernières lignes ne sont pas définies, car elles contiennent des opérations arithmétiques sur des pointeurs en dehors du tableau!


Bien sûr, ce n'est pas le programmeur qui est à blâmer. Le problème vient du standard C ++ lui-même, qui déclare que ce morceau de code évidemment raisonnable est un comportement non défini. Par conséquent, P0593 suggère de corriger la norme en ajoutant certaines fonctions (comme ::operator new et std::malloc ) la possibilité de créer des tableaux selon les besoins. Tous les pointeurs créés par eux deviendront magiquement des pointeurs vers des tableaux, et des opérations arithmétiques peuvent être effectuées avec eux.


Toujours pas sur les noms, attendez une seconde.


Mais parfois, des opérations sur les pointeurs sont nécessaires lorsque vous travaillez avec de la mémoire qu'une de ces fonctions n'a pas allouée. Par exemple, la fonction deallocate() fonctionne essentiellement avec la mémoire morte, dans laquelle il n'y a aucun objet du tout, mais doit toujours additionner le pointeur et la taille de la zone. Pour ce cas, P0593 offrait la fonction std::bless(void* ptr, std::size_t n) (il y avait une autre fonction là-bas, qui est aussi appelée bless , mais ce n'est pas tout). Il n'a aucun effet sur un ordinateur physique réel, mais il crée des objets pour une machine abstraite qui permettraient d'utiliser l'arithmétique des pointeurs.


Le nom std::bless était temporaire.


Donc, le nom.


A Cologne, LEWG a été chargé de trouver un nom pour cette fonction. Les options implicitly_create_objects() et implicitly_create_objects_as_needed() ont été proposées, car c'est ce que fait la fonction.


Je n'ai pas aimé ces options.


Exemple 3: std :: partial_sort_copy ()


Exemple tiré de la présentation de Kate


Il existe une fonction std::sort , qui trie les éléments du conteneur:


 std::vector<int> vec = {3, 1, 5, 4, 2}; std::sort(vec.begin(), vec.end()); // vec == {1, 2, 3, 4, 5} 

Il y a aussi std::partial_sort , qui trie seulement une partie des éléments:


 std::vector<int> vec = {3, 1, 5, 4, 2}; std::partial_sort(vec.begin(), vec.begin() + 3, vec.end()); // vec == {1, 2, 3, ?, ?} ( ...4,5,  ...5,4) 

Et il y a toujours std::partial_sort_copy , qui trie également une partie des éléments, mais en même temps l'ancien conteneur ne change pas, mais transfère les valeurs au nouveau:


 const std::vector<int> vec = {3, 1, 5, 4, 2}; std::vector<int> out; out.resize(3); std::partial_sort_copy(vec.begin(), vec.end(), out.begin(), out.end()); // out == {1, 2, 3} 

Kate prétend que std::partial_sort_copy est un nom moyen, et je suis d'accord avec elle.


Nom de l'implémentation et nom du résultat


Aucun des noms répertoriés n'est, à proprement parler, incorrect : ils décrivent tous parfaitement ce que fait la fonction. std::log2p1() compte vraiment le logarithme binaire et en ajoute un; implicitly_create_objects() crée implicitement des objets et std::partial_sort_copy() trie partiellement le conteneur et copie le résultat. Cependant, je n'aime pas tous ces noms, car ils sont inutiles .


Aucun programmeur ne s'assoit et ne pense: «J'aimerais pouvoir prendre le logarithme binaire et en ajouter un». Il a besoin de savoir combien de bits la valeur donnée conviendra et il cherche sans succès sur les docks quelque chose comme bit_width . Au moment où il atteint l'utilisateur de la bibliothèque, qu'est-ce que le logarithme binaire a à voir avec cela, il a déjà écrit son implémentation (et a probablement manqué la vérification de zéro). Même si std::log2p1 s'est avéré être un miracle dans le code, le prochain à voir ce code devrait à nouveau comprendre ce qu'il est et pourquoi il est nécessaire. bit_width(max_value) n'aurait pas un tel problème.


De même, personne n'a besoin de «créer implicitement des objets» ou de «trier partiellement la copie du vecteur» - ils doivent réutiliser la mémoire ou obtenir les 5 plus grandes valeurs dans l'ordre décroissant. Quelque chose comme recycle_storage() (qui a également été suggéré comme nom std::bless ) et top_n_sorted() serait beaucoup plus clair.


Kate utilise le terme nom d'implémentation pour std::partial_sort_copy() , mais il convient également à deux autres fonctions. L'implémentation de leur nom est vraiment parfaitement décrite. C'est juste que l'utilisateur a besoin du nom du résultat - ce qu'il obtient en appelant la fonction. Pour sa structure interne, il s'en fiche, il veut juste connaître la taille en bits ou réutiliser la mémoire.


Nommer une fonction en fonction de sa spécification signifie créer à l'improviste un malentendu entre le développeur de la bibliothèque et son utilisateur. Vous devez toujours vous rappeler quand et comment la fonction sera utilisée.


Cela semble ringard, oui. Mais à en juger par std::log2p1() , cela est loin d'être évident pour tout le monde. De plus, parfois ce n’est pas si simple.


Exemple 4: std :: popcount ()


std::popcount() , comme std::log2p1() , en C ++ 20, il est proposé d'ajouter à <bit> . Et cela, bien sûr, est un nom monstrueusement mauvais. Si vous ne savez pas ce que fait cette fonction, il est impossible de le deviner. Non seulement l'abréviation est déroutante (il y a pop dans le nom, mais pop / push n'a rien à voir avec cela) - déchiffrer le nombre de personnes (compter la population? Le nombre de populations?) N'aide pas non plus.


D'un autre côté, std::popcount() idéal pour cette fonction car il appelle l'instruction d'assemblage popcount. Ce n'est pas seulement le nom de l' implémentation - c'est sa description complète.


Cependant, dans ce cas, l'écart entre les développeurs de langues et les programmeurs n'est pas si grand. Une instruction qui compte le nombre d'unités dans un mot binaire est appelée un popcount des années soixante. Pour une personne qui sait quoi que ce soit sur les opérations de bits, un tel nom est absolument évident.


Soit dit en passant, une bonne question: pensez-vous aux noms qui conviennent aux débutants, ou les laissez-vous familiers avec les anciens?


Happy End?


P1956 suggère de renommer std::log2p1() en std::bit_width() . Cette proposition est susceptible d'être acceptée en C ++ 20. std::ceil2 et std::floor2 seront également renommés respectivement std :: bit_ceil () et std :: bit_floor (). Leurs anciens noms n'étaient également pas très, mais pour d'autres raisons.


LEWG à Cologne n'a sélectionné ni implicitly_create_objects[_as_needed] ni recycle_storage comme nom pour std::bless . Ils ont décidé de ne pas du tout inclure cette fonction dans la norme. Le même effet peut être obtenu en créant explicitement un tableau d'octets, donc, disent-ils, la fonction n'est pas nécessaire. Je n'aime pas cela car appeler std::recycle_storage() serait plus lisible. Un autre std::bless() existe toujours, mais est maintenant appelé start_lifetime_as . J'aime ça. Il devrait aller en C ++ 23.


Bien sûr, std::partial_sort_copy() ne std::partial_sort_copy() plus renommé - sous ce nom, il est entré dans la norme en 1998. Mais au moins std::log2p1 corrigé, et ce n'est pas mal.


Pour trouver les noms des fonctions, vous devez penser à qui les utilisera et à ce qu'il attend d'eux. Comme l'a dit Kate, la dénomination nécessite de l'empathie .

Source: https://habr.com/ru/post/fr484860/


All Articles