C++ Core Guidelines:Die Standardbibliothek

C++ Core Guidelines:Die Standardbibliothek

Die Regeln der C++-Standardbibliothek beziehen sich hauptsächlich auf Container, Strings und Iostreams.

Seltsamerweise gibt es in diesem Kapitel keinen Abschnitt über die Algorithmen der Standard-Template-Bibliothek (STL). Kurios, denn in der C++-Community gibt es ein Sprichwort:Wer eine explizite Schleife schreibt, kennt die Algorithmen der STL nicht. Wie auch immer. Lassen Sie mich nur der Vollständigkeit halber mit den ersten drei Regeln beginnen, die nicht viel Aufsehen erregen.

SL.1:Verwenden Sie wo immer möglich Bibliotheken, denn das Rad neu erfinden ist keine gute Idee. Außerdem profitieren Sie von der Arbeit anderer. Das bedeutet, dass Sie bereits getestete und klar definierte Funktionalität verwenden. Dies gilt insbesondere dann, wenn Sie SL.2:die Standardbibliothek anderen Bibliotheken vorziehen. Stellen Sie sich zum Beispiel vor, Sie stellen jemanden ein. Der Vorteil ist, dass er die Bibliothek bereits kennt und Sie ihm Ihre Bibliotheken nicht beibringen müssen. Sie sparen viel Geld und Zeit. Ich hatte einmal einen Kunden, der seinen Infrastruktur-Namensraum std nannte. Natürlich, wenn Sie viel Spaß haben wollen, tun Sie es. Wenn nicht:SL.3:Fügen Sie dem Namensraum std keine Nicht-Standard-Entities hinzu .

Die nächsten Regeln für STL-Container sind konkreter.

Container

Die erste Regel ist recht einfach zu argumentieren.

SL.con.1:Bevorzugen Sie die Verwendung von STL array oder vector anstelle eines C-Arrays

Ich nehme an, Sie kennen einen std::vector. Einer der großen Vorteile eines std::vector gegenüber einem C-Array ist, dass der std::vector seinen Speicher automatisch verwaltet. Das gilt natürlich auch für alle weiteren Container der Standard Template Library. Aber schauen wir uns nun die automatische Speicherverwaltung von std::vector genauer an.

std::vector

// vectorMemory.cpp

#include <iostream>
#include <string>
#include <vector>

template <typename T>
void showInfo(const T& t,const std::string& name){

 std::cout << name << " t.size(): " << t.size() << std::endl;
 std::cout << name << " t.capacity(): " << t.capacity() << std::endl;

}

int main(){
 
 std::cout << std::endl;

 std::vector<int> vec; // (1)

 std::cout << "Maximal size: " << std::endl;
 std::cout << "vec.max_size(): " << vec.max_size() << std::endl; // (2)
 std::cout << std::endl;

 std::cout << "Empty vector: " << std::endl;
 showInfo(vec, "Vector");
 std::cout << std::endl;

 std::cout << "Initialised with five values: " << std::endl; 
 vec = {1,2,3,4,5};
 showInfo(vec, "Vector"); // (3)
 std::cout << std::endl;

 std::cout << "Added four additional values: " << std::endl;
 vec.insert(vec.end(),{6,7,8,9});
 showInfo(vec,"Vector"); // (4)
 std::cout << std::endl;

 std::cout << "Resized to 30 values: " << std::endl;
 vec.resize(30);
 showInfo(vec,"Vector"); // (5)
 std::cout << std::endl;

 std::cout << "Reserved space for at least 1000 values: " << std::endl;
 vec.reserve(1000);
 showInfo(vec,"Vector"); // (6)
 std::cout << std::endl;

 std::cout << "Shrinke to the current size: " << std::endl;
 vec.shrink_to_fit(); // (7)
 showInfo(vec,"Vector");

}

Um Tipparbeit zu ersparen habe ich die kleine Funktion showInfo geschrieben. Diese Funktion gibt für einen Vektor seine Größe und seine Kapazität zurück. Die Größe eines Vektors ist seine Anzahl von Elementen, die Kapazität eines Containers ist die Anzahl von Elementen, die ein Vektor ohne zusätzliche Speicherzuweisung aufnehmen kann. Daher muss die Kapazität eines Vektors mindestens so groß sein wie seine Größe. Sie können die Größe eines Vektors mit seiner Methode resize anpassen; Sie können die Kapazität eines Behälters mit seiner Methodenreserve anpassen.

Aber zurück zum Programm von oben nach unten. Ich erstelle (Zeile 1) einen leeren Vektor. Anschließend zeigt das Programm (Zeile 2) die maximale Anzahl von Elementen an, die ein Vektor haben kann. Nach jeder Operation gebe ich ihre Größe und Kapazität zurück. Das gilt für die Initialisierung des Vektors (Zeile 3), für das Hinzufügen von vier neuen Elementen (Zeile 4), die Größenanpassung der Container auf 30 Elemente (Zeile 5) und das Reservieren von zusätzlichem Speicher für mindestens 1000 Elemente ( Zeile 6). Mit C++11 können Sie mit der Methode "shrink_to_fit" (Zeile 7) die Kapazität des Vektors auf seine Größe verkleinern.

Bevor ich die Ausgabe des Programms unter Linux präsentiere, lassen Sie mich einige Bemerkungen machen.

  1. Die Anpassung der Größe und des Fassungsvermögens des Behälters erfolgt automatisch. Ich habe keinerlei Speicheroperationen wie new und dele verwendet
  2. Durch die Verwendung der Methode vec.resize(n) erhält der Vektor vec neue default-initialisierte Elemente, wenn n> cont.size() gilt.
  3. Durch die Verwendung der Methode vec.reserve(n) bekommt der Container vec neuen Speicher für mindestens n Elemente, wenn n> cont.capacity() gilt.
  4. Der Aufruf shine_to_fit ist unverbindlich. Das bedeutet, dass die C++-Laufzeit die Kapazität eines Containers nicht an seine Größe anpassen muss. Aber meine Verwendung der Methode "shrink_to_fit" mit GCC, clang oder cl.exe hat immer den unnötigen Speicher freigegeben.

Okay, aber was ist der Unterschied zwischen einem C-Array und einem C++-Array?

std::array

std::array vereint das Beste aus zwei Welten. Einerseits hat std::array die Größe und Effizienz eines C-Arrays; std::array hingegen hat die Schnittstelle eines std::vector.

Mein kleines Programm vergleicht die Speichereffizienz eines C-Arrays, eines C++-Arrays (std::array) und eines std::vector.

// sizeof.cpp

#include <iostream>
#include <array>
#include <vector>
 
int main(){
 
 std::cout << std::endl;
 
 std::cout << "sizeof(int)= " << sizeof(int) << std::endl;
 
 std::cout << std::endl;
 
 int cArr[10] = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10};
 
 std::array<int, 10> cppArr = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10};
 
 std::vector<int> cppVec = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10};
 
 std::cout << "sizeof(cArr)= " << sizeof(cArr) << std::endl; // (1)
 
 std::cout << "sizeof(cppArr)= " << sizeof(cppArr) << std::endl; // (2)
 
 // (3)
 
 std::cout << "sizeof(cppVec) = " << sizeof(cppVec) + sizeof(int) * cppVec.capacity() << std::endl;
 std::cout << " = sizeof(cppVec): " << sizeof(cppVec) << std::endl;
 std::cout << " + sizeof(int)* cppVec.capacity(): " << sizeof(int)* cppVec.capacity() << std::endl;

 std::cout << std::endl;
 
}

Sowohl das C-Array (Zeile 1) als auch das C++-Array (Zeile 2) nehmen 40 Bytes ein. Das ist genau sizeof(int) * 10. Im Gegensatz dazu benötigt der std::vector zusätzliche 24 Bytes (Zeile 3), um seine Daten auf dem Heap zu verwalten.

Dies war der C-Teil eines std::array, aber das std::array unterstützt die Schnittstelle eines std::vector. Das bedeutet insbesondere, dass std::array seine Größe kennt und daher fehleranfällige Schnittstellen wie die folgende ein starker Code-Smell sind.

void bad(int* p, int count){
 ... 
}

int myArray[100] = {0}; 
bad(myArray, 100);

// ----------------------------- 

void good(std::array<int, 10> arr){
 ...
}

std::array<int, 100> myArray = {0};
good(myArray);

Wenn Sie ein C-Array als Funktionsargument verwenden, entfernen Sie fast alle Typinformationen und übergeben es als Zeiger auf sein erstes Argument. Dies ist extrem fehleranfällig, da Sie die Anzahl der Elemente zusätzlich angeben müssen. Dies gilt nicht, wenn Ihre Funktion ein std::array.

akzeptiert

Wenn das Funktionsgut nicht generisch genug ist, können Sie eine Vorlage verwenden.

template <typename T>
void foo(T& arr){

 arr.size(); // (1)

}


std::array<int, 100> arr{}; 
foo(arr); 
 
std::array<double, 20> arr2{};
foo(arr2); 

Da ein std::array seine Größe kennt, können Sie diese in Zeile 1 erfragen.

Was kommt als nächstes?

Die nächsten beiden Regeln für Container sind recht interessant. Im nächsten Beitrag gebe ich eine Antwort auf die Frage:Wann verwende ich welchen Container?