Anregung für diesen Code war
Mir geht es hier insbesondere um Containertypen mit zusammenhängenden Puffer, für die ein string_view keinen Constructor überladen hat, aber auch darum, gleich Views von Teilstrings zu erstellen.
Im Download:
Im sv_helper.hpp Header finden sich 16 Überladungen des Funktionstemplates
Unter einem stringartigen Objekt sind Objekte der folgenden Klassen zu verstehen
-
-
-
sofern ein
Der Testcode (main.c) ...
... ergibt folgende Ausgabe:
Hierbei würde ich mich insbesondere über Feedback freuen:
Die Verwendung der leeren
Alternativen über die ich schon nachgedacht habe:
- Er sucht das passende Overload. (Und das ist das einzige worum es bei der Aktion geht.)
- Er sieht Move-Semantics durch die rvalue-Referenz und wird im ersten Anlauf die Erstellung des Objekts in die Funktion ziehen wollen (wodurch dessen Lebenszeit ohnehin auf die der Funktion beschränkt wäre).
- Er stellt aber fest, dass es dort keine Parametervariable gibt, dass die Erstellung eines Objekts toter Code ist, und schmeißt das Ganze hochkant raus.
Vielleicht noch ...
Wer sich fragt, von was ich bei einem string_view überhaupt rede (falls noch nicht mit C++17 vertraut):
Ein string_view hat keinen eigenen Stringpuffer. Es ist nicht Eigentümer des Strings.
Typische Implementierung sieht etwa so aus:
Mit einem string_view kann also lediglich der Puffer eines fremden Objekts referenziert werden. Pointer m_data zeigt dabei auf das erste Zeichen für das View (das kann eine beliebige Position im fremden Puffer sein) und m_size gibt an wie viele aufeinanderfolgende Zeichen ab m_data für das View gültig sind.
Das bedeutet, wenn das fremde Objekt zerstört ist, enthält das View einen hängenden Zeiger auf etwas das nicht mehr existiert und nicht etwa eine Kopie des Strings. Gefährlich bei temporären Objekten. Der Pointer kann auch den Wert nullptr haben. Das ist so, wenn der Default-Constructor aufgerufen wird. Dann wird natürlich auch m_size mit 0 initialisiert. Bedeutet, der Pointer eines "leeren" Views sollte nicht dereferenziert werden, weil nicht klar ist, ob m_data auf gültigen Speicher zeigt. Und, anders als data() und c_str() bei der string Klasse, kann man nicht davon ausgehen dass data() eines string_view auf einen nullterminierten Bereich zeigt.
Alles in allem ist ein string_view eine coole Sache, weil Strings nicht mehr hin und her kopiert werden müssen. Das macht durch die nicht erforderlichen Speicherallokationen und O(1) für substr() einen riesigen Performancesprung. Andererseits holt man sich mit Views quasi dieselben Risiken und Probleme ins Haus, die man mit Pointern eben hat
ostream operator<< für String-like Container
Keine Frage, aber auch nicht Wert für einen Beitrag in den Ressourcen ... Bin heute über einen alten Code aus C++11 Zeiten gestolpert, in dem ich operatorsputc(ostr.fill()) == traitsT::eof()) { ostr.setstate(std::basic_ios::badbit); break; }...
dev-community.de
Im Download:
Im sv_helper.hpp Header finden sich 16 Überladungen des Funktionstemplates
sv::make()
für folgende Aufgaben:- Erstellen eines leeren Views.
- Erstellen eines Views von einer definierten Anzahl von Zeichen am Anfang eines Zeichenarrays.
- Erstellen eines Views von einem durch Offset und Länge definierten Bereich in einem Zeichenarray.
- Erstellen eines Views von einem nullterminierten String.
- Erstellen eines Views von einem nullterminierten String, abzüglich einer definierten Anzahl von Zeichen am Anfang.
- Erstellen eines Views von einer definierten Anzahl Zeichen am Ende eines nullterminierten Strings.
- Erstellen einer Kopie eines anderen Views.
- Erstellen eines Views von einer definierten Anzahl von Zeichen am Anfang eines anderen Views.
- Erstellen eines Views von einem durch Offset und Länge definierten Bereich in einem anderen View.
- Erstellen eines Views von einem anderen View, abzüglich einer definierten Anzahl von Zeichen am Anfang.
- Erstellen eines Views von einer definierten Anzahl Zeichen am Ende eines anderen Views.
- Erstellen eines Views vom Puffer eines stringartigen Objekts.
- Erstellen eines Views von einer definierten Anzahl von Zeichen am Anfang des Puffers eines stringartigen Objekts.
- Erstellen eines Views von einem durch Offset und Länge definierten Bereich im Puffer eines stringartigen Objekts.
- Erstellen eines Views vom Puffer eines stringartigen Objekts, abzüglich einer definierten Anzahl von Zeichen am Anfang.
- Erstellen eines Views von einer definierten Anzahl Zeichen am Ende des Puffers eines stringartigen Objekts.
Unter einem stringartigen Objekt sind Objekte der folgenden Klassen zu verstehen
-
std::basic_string<charT>
-
std::array<charT, N>
-
std::vector<charT>
sofern ein
std::basic_string_view
mit dem jeweiligen charT
Typ erstellbar ist.Der Testcode (main.c) ...
C++:
#include "sv_helper.hpp"
#include <string>
#include <array>
#include <vector>
#include <iostream>
// #include <deque>
int main()
{
const std::array arr{ 'd', 'u', 'm', 'm', 'y' };
const std::vector<char> vec{ arr.cbegin(), arr.cend() };
const std::string str{ arr.data(), arr.size() };
const std::string_view svw{ str };
const char* const ptr{ arr.data() }; // ohne Nullterminierung
const char* const ntstr{ str.c_str() }; // nullterminierter String
// +++ Das soll nicht kompilieren, da ein std::deque keinen zusammenhängenden Puffer hat:
// const std::deque<char> deq{ arr.cbegin(), arr.cend() };
// const auto err{ sv::make(deq) }; // keine Überladung gefunden, denn ein std::deque hat keine data() Methode
std::cout <<
sv::make(ptr, 2) << '\n' << // die ersten 2 Zeichen des Strings
sv::make(1, ptr, 2) << '\n' << // 1 Zeichen übersprungen, folgende 2 Zeichen des Strings
sv::make(2, ntstr) << '\n' << // 2 Zeichen übersprungen, Rest des Strings (ACHTUNG: Nullterminierung erforderlich)
sv::make(ntstr, 2, sv::tail{}) << '\n' << // 2 Zeichen vom Ende des Strings (ACHTUNG: Nullterminierung erforderlich)
sv::make(ntstr) << '\n' << // kompletter String (ACHTUNG: Nullterminierung erforderlich)
sv::make<char>() << '\n' << // leer
sv::make(svw, 2) << '\n' << // die ersten 2 Zeichen des Views
sv::make(1, svw, 2) << '\n' << // 1 Zeichen übersprungen, folgende 2 Zeichen
sv::make(2, svw) << '\n' << // 2 Zeichen übersprungen, Rest des Views
sv::make(svw, 2, sv::tail{}) << '\n' << // 2 Zeichen vom Ende des Views
sv::make(svw) << '\n' << // Kopie des Views
sv::make(sv::make<char>()) << '\n' << // Kopie eines leeren Views
sv::make(str, 2) << '\n' << // die ersten 2 Zeichen des genutzten Puffers
sv::make(1, arr, 2) << '\n' << // 1 Zeichen übersprungen, folgende 2 Zeichen des genutzten Puffers
sv::make(2, vec) << '\n' << // 2 Zeichen übersprungen, Rest des genutzten Puffers
sv::make(vec, 2, sv::tail{}) << '\n' << // 2 Zeichen vom Ende des genutzten Puffers
sv::make(vec) << std::endl; // kompletter genutzter Puffer
}
Hierbei würde ich mich insbesondere über Feedback freuen:
Die Verwendung der leeren
sv::tail
Klasse, in der Bedeutung für "starte am Stringende", ist auf den ersten Blick nicht so ganz sauber. Vielleicht fällt jemand von euch was besseres ein?Alternativen über die ich schon nachgedacht habe:
- Das
sv::tail
in die Templateparameter ziehen. Müsste dann aber der erste sein, was sich mit den restlichen Überladungen beißt. Auch beim Aufruf müsste das immer als Template geschrieben werden, was wiederum nicht der sonst üblichen Verwendung entspricht, bei der es durch Template Parameter Deduction unnötig ist, irgendwas an die Template Parameter zu übergeben. - Eine weitere Funktion schreiben, z.B. sv::rmake. Ist dann aber ... eine weitere Funktion. (Allerdings vermutlich die sauberste Lösung.)
sv::tail
Objekt mit einem Byte Größe erzeugen. Real passiert das natürlich nicht:- Er sucht das passende Overload. (Und das ist das einzige worum es bei der Aktion geht.)
- Er sieht Move-Semantics durch die rvalue-Referenz und wird im ersten Anlauf die Erstellung des Objekts in die Funktion ziehen wollen (wodurch dessen Lebenszeit ohnehin auf die der Funktion beschränkt wäre).
- Er stellt aber fest, dass es dort keine Parametervariable gibt, dass die Erstellung eines Objekts toter Code ist, und schmeißt das Ganze hochkant raus.
Vielleicht noch ...
Wer sich fragt, von was ich bei einem string_view überhaupt rede (falls noch nicht mit C++17 vertraut):
Ein string_view hat keinen eigenen Stringpuffer. Es ist nicht Eigentümer des Strings.
Typische Implementierung sieht etwa so aus:
C++:
template<typename charT, typename traitsT = ::std::char_traits<charT>>
class basic_string_view
{
public:
// Typen, `npos`, Konstruktoren, Operatoren, Methoden
private:
const charT* m_data; // die data() Methode gibt diesen Pointer zurück
size_t m_size; // die size() Methode gibt diesen Wert zurück
};
Mit einem string_view kann also lediglich der Puffer eines fremden Objekts referenziert werden. Pointer m_data zeigt dabei auf das erste Zeichen für das View (das kann eine beliebige Position im fremden Puffer sein) und m_size gibt an wie viele aufeinanderfolgende Zeichen ab m_data für das View gültig sind.
Das bedeutet, wenn das fremde Objekt zerstört ist, enthält das View einen hängenden Zeiger auf etwas das nicht mehr existiert und nicht etwa eine Kopie des Strings. Gefährlich bei temporären Objekten. Der Pointer kann auch den Wert nullptr haben. Das ist so, wenn der Default-Constructor aufgerufen wird. Dann wird natürlich auch m_size mit 0 initialisiert. Bedeutet, der Pointer eines "leeren" Views sollte nicht dereferenziert werden, weil nicht klar ist, ob m_data auf gültigen Speicher zeigt. Und, anders als data() und c_str() bei der string Klasse, kann man nicht davon ausgehen dass data() eines string_view auf einen nullterminierten Bereich zeigt.
Alles in allem ist ein string_view eine coole Sache, weil Strings nicht mehr hin und her kopiert werden müssen. Das macht durch die nicht erforderlichen Speicherallokationen und O(1) für substr() einen riesigen Performancesprung. Andererseits holt man sich mit Views quasi dieselben Risiken und Probleme ins Haus, die man mit Pointern eben hat