Icon Ressource

[C++] 'sv::make' Convenience-Funktion zur Erstellung von string_view Objekten 2020-04-19

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 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
}
... ergibt folgende Ausgabe:
1587339752699.png


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:
  1. 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.
  2. Eine weitere Funktion schreiben, z.B. sv::rmake. Ist dann aber ... eine weitere Funktion. (Allerdings vermutlich die sauberste Lösung.)
Unter dem Strich ist die derzeitige Implementation aber auch völlig unkritisch. Natürlich könnte der Compiler theoretisch tatsächlich ein 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 ;)
Autor
german
Downloads
217
Aufrufe
774
Erstellt am
Letzte Bearbeitung
Bewertung
0,00 Stern(e) 0 Bewertung(en)

Weitere Ressourcen von german

Oben Unten