C++ Core Guidelines: Regeln zu Strings

Modernes C++  –  9 Kommentare

Die C++ Core Guidelines verwenden den Begriff String als eine Sequenz von Zeichen. Konsequenterweise beschäftigen sich daher die Guidelines mit dem C-String, dem C++-String, dem C++17 std::string_view und dem C++17 std::byte.

Ich werde mich in diesem Artikel nicht allzu streng an die Guidelines halten und die Strings ignorieren, die wie gsl.:string_span, zstring und czstring Bestandteil der Guidelines Support Library sind. Der Einfachheit halber bezeichne ich den std::string als C++-String und const char* als C-String.

Los geht es mit der ersten Regel.

SL.str.1: Use std::string to own character sequences

Ich vermute, du kennst einen anderen String, der eine Zeichensequenz besitzt: den C-String. Verwende keinen C-String! Warum? Mit einem C-String musst du dich um das Speichermanagement, das String-Endzeichen und die Länge des String selbst kümmern:

// stringC.c

#include <stdio.h>
#include <string.h>

int main( void ){

char text[10];

strcpy(text, "The Text is too long for text."); // (1) text is too big
printf("strlen(text): %u\n", strlen(text)); // (2) text has no termination character '\0'
printf("%s\n", text);

text[sizeof(text)-1] = '\0';
printf("strlen(text): %u\n", strlen(text));

return 0;
}

Das einfache Programm stringC.c besitzt in der Zeile (1) und in der Zeile (2) undefiniertes Verhalten. Wird es mit einem leicht angestaubten GCC 4.8 übersetzt, scheint alles zu funktionieren:

Die C++-Variante besitzt nicht die gleichen Probleme:

// stringCpp.cpp 

#include <iostream>
#include <string>

int main(){

std::string text{"The Text is not too long."};

std::cout << "text.size(): " << text.size() << std::endl;
std::cout << text << std::endl;

text +=" And can still grow!";

std::cout << "text.size(): " << text.size() << std::endl;
std::cout << text << std::endl;

}

Die Ausgabe des Programms sollte nicht überraschend sein:

Im Fall des C++-Strings gibt es keine Möglichkeit, Fehler zu machen. Die C++-Laufzeit kümmert sich automatisch um das Speichermanagement und das String-Endzeichen. Wenn du darüber hinaus noch auf die Elemente des C++-Strings mit dem at- und nicht mit dem Index-Operator zugreifst, sind Zugriffe außerhalb des C++-Strings nicht möglich. Die Details zum at-Operator lassen sich schön in meinem vorherigen Artikel "C++ Core Guidelines: Greife nicht über den Container hinaus" nachlesen.

Weißt du, was seltsam in C++ einschließlich C++11 war? Es gab keine Möglichkeit, einen C++-String zu erzeugen, der nicht von einem C-String ausging. Dies war seltsam, da doch der C++-String eingeführt wurde, um den C-String loszuwerden. Diese Inkonsistenz gibt es nicht mehr mit C++14.

SL.str.12: Use the s suffix for string literals meant to be standard-library strings

Mit C++14 haben wir C++-String-Literale erhalten. Dies ist eine C-String-Literal mit einem Suffix s: "cStringLiteral"s.

Das nächste Beispiel bringt meine Kernaussage auf den Punkt: C-String-Literale sind keine C++-String-Literale:

// stringLiteral.cpp

#include <iostream>
#include <string>
#include <utility>

int main(){

using namespace std::string_literals; // (1)

std::string hello = "hello"; // (2)

auto firstPair = std::make_pair(hello, 5);
auto secondPair = std::make_pair("hello", 15); // (3)
// auto secondPair = std::make_pair("hello"s, 15); // (4)

if (firstPair < secondPair) std::cout << "true" << std::endl;

}

Es ist schade, dass ich den Namensraum std::string_literals in Zeile (1) inkludieren muss, um C++-String-Literale zu verwenden. Die Zeile (2) ist die entscheidende Zeile des Beispiels. Ich verwende in ihr ein C-String-Literal "hello", um den C++-String zu erzeugen. Dies ist der Grund, dass der Datentyp von firstPair (std::string, int), aber der Datentyp von secondPair (const char*, int) ist. Daher schlägt der Vergleich in der Zeile (5) fehl, denn unterschiedliche Datentypen können nicht verglichen werden. Betrachte dazu sorgfältig die letzte Zeile der folgenden Fehlermeldung:

Wenn ich hingegen den C++-String-Literal in Zeile (4) anstelle des C-String-Literals in Zeile (3) verwende, verhält sich das Programm wie erwartet:

C++-String-Literale waren ein C++14-Feature. Nun springe ich drei Jahre weiter in die Zukunft. Mit C++17 haben wir std::string_view und std::byte erhalten. Insbesondere zu std::string_view habe ich schon einiges geschrieben. Daher werde ich nur die wichtigsten Fakten wiederholen.

SL.str.2: Use std::string_view or gsl::string_span to refer to character sequences

Ein std::string_view referenziert nur eine Zeichensequenz. Gerne drücke ich das noch expliziter aus: Ein std::string_view besitzt nicht eine Zeichensequenz. Er stellt nur eine Sicht auf Zeichensequenzen dar. Diese Zeichensequenz kann ein C++- oder ein C-String sein. Ein std::string_view benötigt nur zwei Informationen: den Zeiger auf die Zeichensequenz und deren Länge. Er bietet das lesende Teil des Interface eines std::string an. Zusätzlich zu einem std::string unterstützt er zwei modifizierende Operationen: remove_prefix und remove_suffix.

Vermutlich wunderst du dich gerade: Warum benötigen wir einen std::string_view? Ein std::string_view ist sehr billig zu kopieren und benötigt keinen Speicher. Mein früherer Artikel "Vermeide Kopieren mit std::string_view" zeigt die beeindruckenden Performanzzahlen eines std::string_view.

Wie ich es bereits erwähnt habe, haben wir in C++17 auch den neuen Datentyp std::byte erhalten.

SL.str.4: Use char* to refer to a single character and SL.str.5: Use std::byte to refer to byte values that do not necessarily represent characters

Wenn du nicht der Regel str.4 folgst und const char* als C-String verwendest, findest du dich vielleicht in einer ähnlichen Situation wieder:

char arr[] = {'a', 'b', 'c'}; 

void print(const char* p) {
cout << p << '\n';
}

void use() {
print(arr); // run-time error; potentially very bad
}

arr wird auf einen Zeiger reduziert, falls er als Argument der Funktion print verwendet wird. Das undefinierte Verhalten ist, dass arr kein String-Endzeichen besitzt. Falls du nun glaubst, du kannst std::byte als ein Zeichen verwendet, ist dies falsch.

std::byte ist ein Datentyp, der das Konzept eines Bytes umsetzt, wie es im C++-Standard definiert wird. Nun wissen wir, was ein Byte ist. Es ist nicht eine Ganzahl oder ein Buchstabe und daher gegen diese Art von Fehler gefeit. Sein Job ist es, auf Speicher zuzugreifen. Konsequenterweise besteht sein Interface nur aus Methoden für bitweise, logische Operationen:

namespace std { 

template <class IntType>
constexpr byte operator<<(byte b, IntType shift);
template <class IntType>
constexpr byte operator>>(byte b, IntType shift);
constexpr byte operator|(byte l, byte r);
constexpr byte operator&(byte l, byte r);
constexpr byte operator~(byte b);
constexpr byte operator^(byte l, byte r);

}

Du kannst die Funktion std::to_integer(std::byte b) verwenden, um einen std::byte in eine Ganzzahl zu konvertieren. Der Aufruf std::byte{integer} konvertiert genau in die andere Richtung. integer muss eine positive Zahl sein, die kleiner als std::numeric_limits<unsigned_char>::max() ist.

Wie geht's weiter?

Ich habe nahezu alle Regeln zur C++-Standardbibliothek beschrieben. Lediglich ein paar Regeln zu den Ein- und Ausgabestreams und der C-Standardbibliothek fehlen noch. Du kannst dir denken, worüber ich meinen nächsten Artikel schreibe.