Implementierungsname und Ergebnisname


Ich wollte diesen Beitrag schon im Juli schreiben, konnte mich aber, oh Ironie , nicht entscheiden, wie ich ihn nennen soll. Erst nach Kate Gregorys Vortrag auf der CppCon kamen mir gute Begriffe in den Sinn, und jetzt kann ich Ihnen endlich sagen, wie man Funktionen aufruft .


Natürlich gibt es Namen, die überhaupt keine Informationen enthalten, wie z. B. int f(int x) . Sie müssen auch nicht verwendet werden, aber es geht nicht um sie. Manchmal scheint es, dass die Informationen im Titel voll sind, aber es gibt absolut keinen Nutzen daraus.


Beispiel 1: std :: log2p1 ()


In C ++ 20 wurden dem Header mehrere neue Funktionen für Bitoperationen hinzugefügt, unter anderem std::log2p1 . Es sieht so aus:


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

Das heißt, für jede natürliche Zahl gibt die Funktion ihren binären Logarithmus plus 1 zurück, und für 0 gibt sie 0 zurück. Und dies ist keine Schulaufgabe für den Operator if / else, das ist wirklich nützlich - die minimale Anzahl von Bits, in die dieser Wert passt. Es ist fast unmöglich, nur den Namen der Funktion zu erraten.


Beispiel 2: std :: bless ()


Jetzt geht es nicht mehr um den Namen


Ein kleiner Exkurs: In C ++ funktioniert Zeigerarithmetik nur mit Zeigern auf Array-Elemente. Was im Prinzip logisch ist: Im allgemeinen Fall ist die Menge der Nachbarobjekte unbekannt und "in zehn Bytes rechts von der Variablen i kann alles passieren". Dies ist eindeutig vages Verhalten.


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

Eine solche Einschränkung deklariert jedoch einen großen Teil des vorhandenen Codes als unbestimmt. Hier ist zum Beispiel eine vereinfachte Implementierung von 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; //    } 

Wir haben Speicher zugewiesen, alle Objekte verschoben und versuchen nun sicherzustellen, dass die Zeiger angeben, wohin sie gehen sollen. Hier sind nur die letzten drei Zeilen undefiniert, da sie arithmetische Operationen auf Zeiger außerhalb des Arrays enthalten!


Natürlich ist nicht der Programmierer schuld. Das Problem liegt im C ++ - Standard selbst, der dieses offensichtlich vernünftige Stück Code als undefiniertes Verhalten deklariert. Daher schlägt P0593 vor, den Standard zu korrigieren, indem einige Funktionen (wie ::operator new und std::malloc ) hinzugefügt werden, mit denen Arrays nach Bedarf erstellt werden können. Alle von ihnen erstellten Zeiger werden auf magische Weise zu Zeigern auf Arrays, und arithmetische Operationen können mit ihnen ausgeführt werden.


Noch immer nicht über die Namen, warte eine Sekunde.


Manchmal sind jedoch Operationen an Zeigern erforderlich, wenn mit Speicher gearbeitet wird, den eine dieser Funktionen nicht zugewiesen hat. Beispielsweise funktioniert die Funktion deallocate() Wesentlichen mit totem Speicher, in dem sich überhaupt keine Objekte befinden, der Zeiger und die Größe des Bereichs jedoch addiert werden müssen. Für diesen Fall bot P0593 die Funktion std::bless(void* ptr, std::size_t n) (es gab dort eine andere Funktion, die auch bless , aber darum geht es nicht). Es hat keine Auswirkung auf einen realen physischen Computer, erstellt jedoch Objekte für eine abstrakte Maschine, die die Verwendung von Zeigerarithmetik ermöglichen.


Der Name std::bless war vorübergehend.


Also der Name.


In Köln wurde die LEWG beauftragt, einen Namen für diese Funktion zu finden. Die Optionen implicitly_create_objects() und implicitly_create_objects_as_needed() wurden vorgeschlagen, da dies die Funktion ausführt.


Diese Optionen haben mir nicht gefallen.


Beispiel 3: std :: partial_sort_copy ()


Beispiel aus Kates Präsentation


Es gibt eine Funktion std::sort , die die Elemente des Containers sortiert:


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

Es gibt auch std::partial_sort , das nur einen Teil der Elemente sortiert:


 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) 

Und trotzdem gibt es std::partial_sort_copy , das auch einen Teil der Elemente sortiert, gleichzeitig aber den alten Container nicht ändert, sondern die Werte in den neuen überträgt:


 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 behauptet, dass std::partial_sort_copy ein std::partial_sort_copy Name ist, und ich stimme ihr zu.


Implementierungsname und Ergebnisname


Keiner der aufgelisteten Namen ist streng genommen falsch : Sie alle beschreiben perfekt, was die Funktion tut. std::log2p1() zählt wirklich den binären Logarithmus und addiert einen dazu; implicitly_create_objects() erstellt implizit Objekte, und std::partial_sort_copy() sortiert den Container teilweise und kopiert das Ergebnis. Ich mag jedoch nicht alle diese Namen, weil sie unbrauchbar sind .


Kein Programmierer sitzt und denkt: "Ich wünschte, ich könnte den binären Logarithmus nehmen und einen hinzufügen." Er muss wissen, wie viele Bits der angegebene Wert passen wird, und er durchsucht die Docks erfolglos nach etwas wie bit_width . Als er den Bibliotheksbenutzer erreicht, hat er bereits seine Implementierung geschrieben (und höchstwahrscheinlich die Prüfung auf Null verpasst), was der binäre Logarithmus damit zu tun hat. Auch wenn sich std::log2p1 als ein Wunder im Code herausstellte, sollte der nächste, der diesen Code sieht, wieder verstehen, was es ist und warum es benötigt wird. bit_width(max_value) hätte ein solches Problem nicht.


Ebenso muss niemand "implizit Objekte erstellen" oder "die Kopie des Vektors teilweise sortieren" - er muss den Speicher wiederverwenden oder die 5 größten Werte in absteigender Reihenfolge abrufen. Etwas wie recycle_storage() (was auch als Name std::bless ) und top_n_sorted() wären viel klarer.


Kate verwendet den Begriff Implementierungsname für std::partial_sort_copy() , passt aber auch auf zwei andere Funktionen. Die Umsetzung ihres Namens ist wirklich perfekt beschrieben. Der Benutzer benötigt lediglich den Namen des Ergebnisses - das, was er durch Aufrufen der Funktion erhält. Um ihre interne Struktur kümmert es ihn nicht, er will nur die Größe in Bits herausfinden oder den Speicher wiederverwenden.


Eine Funktion anhand ihrer Spezifikation zu benennen, bedeutet, aus heiterem Himmel ein Missverständnis zwischen dem Entwickler der Bibliothek und ihrem Benutzer zu schaffen. Sie müssen sich immer daran erinnern, wann und wie die Funktion verwendet wird.


Das klingt blöd, ja. Aber nach std::log2p1() urteilen, ist dies std::log2p1() für jedermann offensichtlich. Außerdem ist es manchmal nicht so einfach.


Beispiel 4: std :: popcount ()


std::popcount() wird wie std::log2p1() in C ++ 20 vorgeschlagen, <bit> hinzuzufügen. Und das ist natürlich ein ungeheuer schlechter Name. Wenn Sie nicht wissen, was diese Funktion bewirkt, können Sie sie nicht erraten. Nicht nur die Abkürzung ist verwirrend (es gibt Pop im Namen, aber Pop / Push hat nichts damit zu tun) - Entschlüsselung der Bevölkerungszahl (Zählung der Bevölkerung? Die Anzahl der Populationen?) Auch nicht hilfreich.


Auf der anderen Seite ist std::popcount() ideal für diese Funktion, da es die Assembler-Anweisung popcount aufruft. Dies ist nicht nur der Name der Implementierung, sondern deren vollständige Beschreibung.


In diesem Fall ist die Kluft zwischen Sprachentwicklern und Programmierern jedoch nicht so groß. Eine Anweisung, die die Anzahl der Einheiten in einem Binärwort zählt, wird als Popcount aus den sechziger Jahren bezeichnet. Für eine Person, die etwas über Bitoperationen weiß, ist ein solcher Name absolut offensichtlich.


Übrigens eine gute Frage: Findest du Namen, die für Anfänger geeignet sind, oder kennst du sie für Oldfags?


Happy End?


P1956 schlägt vor, std::log2p1() in std::bit_width() . Dieser Vorschlag wird wahrscheinlich in C ++ 20 angenommen. std::ceil2 und std::floor2 werden ebenfalls in std :: bit_ceil () bzw. std :: bit_floor () umbenannt. Ihre alten Namen waren auch nicht sehr, aber aus anderen Gründen.


Die LEWG in Köln wählte weder implicitly_create_objects[_as_needed] noch recycle_storage als Namen für std::bless . Sie beschlossen, diese Funktion überhaupt nicht in den Standard aufzunehmen. Der gleiche Effekt kann erzielt werden, indem explizit ein Array von Bytes erstellt wird. Daher wird die Funktion nicht benötigt. Das gefällt mir nicht, weil der Aufruf von std::recycle_storage() besser lesbar wäre. Ein weiteres std::bless() existiert noch, heißt aber jetzt start_lifetime_as . Das gefällt mir Es sollte in C ++ 23 gehen.


Natürlich wird std::partial_sort_copy() nicht mehr umbenannt - unter diesem Namen wurde es 1998 zum Standard. Aber zumindest wurde std::log2p1 behoben, und das ist nicht schlecht.


Wenn Sie die Namen von Funktionen festlegen, müssen Sie darüber nachdenken, wer sie verwenden wird und was er von ihnen will. Wie Kate es ausdrückte, erfordert das Benennen Einfühlungsvermögen .

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


All Articles