Halten Sie Ihre Container an die Konventionen der STL

Halten Sie Ihre Container an die Konventionen der STL

Eines Tages musste ich ein kleines Refactoring durchführen, das darin bestand, eine Methode namens getSize() umzubenennen in size() , weil ich seine Klasse an generischen Code übergeben musste, der eine Methode size() erwartete . Und was dieses Refactoring ein wenig besonders machte, war, dass diese Klasse sehr verwendet wurde weit über eine ziemlich große Codebasis.

Das ist nicht etwas, wofür Sie Zeit aufwenden möchten, oder?

Es hätte vermieden werden können, wenn die Klasse von Anfang an mit den Konventionen der STL entworfen worden wäre, wo alle Container einen .size() haben Methode. Diese Episode der STL-Lernressource erinnert an die Bedeutung von Konventionen, insbesondere derjenigen der STL.

Die Bedeutung der Einhaltung von Konventionen

Konventionen erleichtern das Verständnis des Codes etwas

Wenn sich der Leser einem bestimmten Codestück nähert, muss er mindestens zwei Arten von Informationen analysieren, um es zu verstehen:seine Semantik und den Stil, in dem es geschrieben ist.

Während wir als Entwickler alle unsere einzigartigen Stile haben (jemals auf ein Stück Code geschaut und gedacht, „das sieht nicht nach mir aus“?), kann einiges davon zwischen den Leuten, die an derselben Codebasis arbeiten, harmonisiert werden, indem Kodierungskonventionen .

Diese Stilelemente, die von allen Entwicklern eines Projekts geteilt werden, nehmen Ihnen einen Teil der Last ab, die Sie beim Lesen von Code herausfinden müssen.

Konventionen erstrecken sich über eine Vielzahl von Themen.

Sie können so gedankenlos entscheiden, wo die öffnende Klammer eines Blocks platziert werden soll:am Ende einer Zeile:

if (condition) {
    ...
}

oder am Anfang einer neuen Zeile:

if (condition)
{
    ...
}

Bei diesem speziellen Beispiel scheint jedoch keines objektiv am besten zu sein. In Code Complete erwähnt Steve McConnell eine Studie, die „in Bezug auf die Verständlichkeit keinen statisch signifikanten Unterschied zwischen den beiden feststellte“. Er sagt weiter:„Sobald Sie sich für einen Stil entschieden haben, profitieren Sie am meisten von einem guten Layout, wenn Sie es konsequent anwenden .“ Daher die Idee, eine Konvention zu haben und sich daran zu halten.

Aber bei Konventionen geht es nicht nur um das Layout, und einige sind näher an der Semantik, wie wir in einer Minute mit der STL sehen werden.

Generischer Code beruht auf Konventionen

Wenn Sie möchten, dass Ihr Code mit einem Teil des Vorlagencodes kompatibel ist, muss er genau die Namen haben, die der Vorlagencode erwartet. Ein solcher Name könnte size sein zum Beispiel. Dies gilt für die heutigen Vorlagen, die Ententypisierung ausführen, und sollte auch dann gelten, wenn Konzepte in die Sprache gelangen.

Der Name an sich spielt keine Rolle. Wichtig ist, dass Template- und Client-Code dieselbe Referenz haben.

Beachten Sie, dass dies auch dann zutrifft, wenn Sie keine Vorlagen verwenden zu viel in deinem Code. Sie könnten von generischem Code profitieren, der dies tut, wie z. B. die STL-Algorithmen, und der mit Ihren Klassen fantastische Dinge leisten könnte, wenn Sie es nur zulassen würden, indem Sie bestimmten Konventionen folgen.

Aus Sicht des Implementierers

Auf der anderen Seite ist es beim Schreiben von generischem Code hilfreich, darüber nachzudenken, welche Konventionen unser Teil des Vorlagencodes instanziiert werden muss. Das sollen Begriffe explizit machen, wenn sie in die Sprache eingehen.

Um den Vorlagencode für so viele Clients wie möglich nutzbar zu machen, können wir versuchen, einige der Anforderungen an den Clientcode zu erleichtern. Wir könnten beispielsweise die Verwendung von std::distance(begin(x), end(x)) in Betracht ziehen statt x.size . Boost Ranges tut dies zum Beispiel.

Oder wir können sogar Funktionen erstellen, die erkennen, welche Funktionalitäten der Client-Code hat, und die verwenden, die er hat.

Die Konventionen der STL 

Beim Erstellen einer Containerklasse bietet das Befolgen der Konventionen der STL-Container zwei Vorteile:

  • sie machen es einem an STL gewöhnten Leser leicht, es zu verstehen wie man die Klasse benutzt,
  • Sie erlauben die Wiederverwendung generischer Code, der auf Containern ausgeführt wird, einschließlich Standardalgorithmen und hausgemachten Bibliotheken.

Hier sind einige Konventionen, die von STL-Containern verwendet werden und denen Ihre Containerklassen folgen sollten.

begin und end

Wie wir beim Design der STL gesehen haben, ist das profitabelste Feature, das wir unseren Containerklassen hinzufügen können, wahrscheinlich das Hinzufügen von begin und end Methoden dazu. Dadurch sind unsere Klassen mit den leistungsstarken STL-Algorithmen kompatibel. Weitere Einzelheiten darüber, was diese Methoden zurückgeben sollten, finden Sie im Artikel.

size

Das war unser motivierendes Beispiel. Betrachten wir zur Veranschaulichung den std::equal Algorithmus, der die Elemente zweier Sammlungen vergleicht und true zurückgibt wenn es jeweils gleich ist.

Wie alle STL-Algorithmen ist std::equal nimmt begin- und end-Iteratoren. Um es mit der Bereichssemantik zu verbessern und zwei Sammlungen direkt zu akzeptieren, können wir es so umschließen:

template<typename Range1, typename Range2>
bool equal(Range1 const& range1, Range2 const& range2)
{
    return std::equal(begin(range1), end(range1), begin(range2));
}

Jedoch vor C++14, std::equal ist einer der „1,5-Bereiche“-Algorithmen, was bedeutet, dass er nur den Beginn-Iterator der zweiten Sequenz und nicht das Ende verwendet. Wenn also der erste Bereich länger als der zweite ist, wird der Algorithmus über die Grenzen des zweiten Bereichs hinaus fortgesetzt, was zu undefiniertem Verhalten führt.

Eine Möglichkeit, um sicherzustellen, dass dies nicht passiert, besteht darin, zu überprüfen, ob die beiden Bereiche dieselbe Größe haben. Wenn dies nicht der Fall ist, müssen keine Elemente verglichen werden, da wir sicher wissen, dass wir false zurückgeben sollten .

Eine mögliche Lösung könnte also sein:

template<typename Range1, typename Range2>
bool equal(Range1 const& range1, Range2 const& range2)
{
    if (range1.size() != range2.size()) return false;

    return std::equal(begin(range1), end(range1), begin(range2));
}

Dies ruft die Methode size auf , das auf allen STL-Containern funktioniert. Um diese Version von equal zu erstellen auch an Ihren Containern arbeiten, müssten sie eine Methode namens size implementieren . Nicht getSize , noch irgendein anderer Name.

Auch wenn wir in diesem speziellen Beispiel die Erstellung von equal in Betracht ziehen könnten Verlassen Sie sich auf andere Möglichkeiten, um die Größe der Bereiche zu erhalten (wie oben beschrieben), und folgen Sie dabei der Konvention von size name macht es wahrscheinlicher, dass Ihr Code mit dieser Art von generischem Code funktioniert.

BEARBEITEN:Beachten Sie, wie von Malcolm im Kommentarbereich festgestellt, dass wir in C++17 std::size(range1) verwenden könnten .

push_back

Um eine Methode hinzuzufügen, die ein Element am Ende Ihrer Containerklasse einfügt, nennen Sie sie push_back . Nicht pushBack noch add noch nicht einmal append . Nur push_back .

Dadurch wird Ihre Klasse mit std::back_inserter kompatibel , wodurch der Container als Ausgabe eines Algorithmus verwendet werden kann, z. B. std::transform zum Beispiel. Tatsächlich std::back_inserter bindet an einen Container und ruft dessen push_back auf Methode, wann immer sie ein Element sendet:

std::vector<int> numbers = {1, 2, 3, 4, 5};
MyCollection results;
std::transform(begin(numbers), end(numbers), std::back_inserter(results), [](int number) { return number * 2; });

// compiles only if MyCollection has a push_back method

insert

Ähnlich dem push_back Methode zur Verwendung von std::back_inserter , std::inserter benötigt eine Methode namens insert und das erfordert zwei Parameter:die einzufügende Position und den einzufügenden Wert, in dieser Reihenfolge.

Für sortierte Container macht es keinen Sinn, eine Position zum Einfügen zu verlangen (es sei denn, der Client-Code weiß es und gibt dem Container einen Hinweis). Jedoch std::inserter erfordert eine Position zum Einfügen, unabhängig davon. Wenn Sie einen Insert-Iterator für einen sortierten Container benötigen, aktivieren Sie sorted_inserter das erfordert keine Position zum Einfügen.

clear

Alle STL-Container haben einen clear Methode, die alle ihre Elemente entfernt. Dies ist ebenfalls eine Konvention, also kein removeAll , clean und nicht einmal Clear mit einem Großbuchstaben.

erase und remove

Das Entfernen einiger Komponenten in einem STL-Container ist ein Thema, das so umfangreich ist, dass es einen eigenen Artikel verdient.

Aber zur Konvention, die meisten STL-Container haben einen erase Methode zum Entfernen von Elementen, außer std::list und std::forward_list die einen remove haben Methode. Aber diese beiden Behälter werden sowieso praktisch nie benutzt.

Ein ganzzahliger Wert in einem Konstruktor bedeutet Größe, nicht Kapazität

Einige STL-Container einschließlich std::vector haben einen Konstruktor, der einen size_t akzeptiert Parameter. Dieser Konstruktor erstellt einen Vektor mit so vielen Elementen, die standardmäßig konstruiert sind (auf ihrem Konstruktor, der keinen Parameter annimmt).

Ich habe benutzerdefinierte Container gesehen, die einen size_t annehmen in ihrem Konstruktor, aber das hat etwas anderes bewirkt, z. B. das Zuweisen eines Speicherpuffers, um so viele Elemente ohne zusätzliche Zuweisung speichern zu können. Anders gesagt, dieser Parameter im Konstruktor dieser Klasse hatte die Semantik einer Kapazität , während die in std::vector hat die Semantik einer Größe . Die Nichteinhaltung dieser Norm führt zu Verwirrung.

Aliase

STL-Container verfügen über eine Reihe von Aliasen oder verschachtelten Klassen, die es generischem Code ermöglichen, Informationen zu Typen abzurufen. Dazu gehört iterator , value_type usw.

Wenn Sie möchten, dass ein solcher generischer Code auch Informationen aus Ihrem Container abruft, sollte er ähnliche Aliase mit genau denselben Namen haben.

class MyContainer
{
public:
    using value_type = // your value type
    using iterator = // your iterator type
    // ...
};

Man erntet, was man sät…

… also, wenn Sie nicht Verwirrung, dumme Refactorings und keine Kompatibilität mit mächtigen bestehenden Bibliotheken ernten wollen, entwerfen Sie Ihre Klassen, indem Sie Konventionen befolgen.

Die oben genannten sind diejenigen, die beim Entwerfen eines Containers zu befolgen sind. Und bitte lassen Sie es mich wissen, wenn Sie einen sehen, den ich vergessen habe, in diese Liste aufzunehmen!

Das könnte dir auch gefallen

  • Die STL-Lernressource