Az std::string_view osztály
Czirkos Zoltán · 2022.06.21.
Hány sztring osztályunk van egy projekten belül? Mennyi bosszúságot okoz ez? Hogy segíthetünk ezen?
C++ tanulmányaink során elég nehéz megúszni egy sztring osztály elkészítését. Vagy többét. A sztring igazi állatorvosi ló, amelyen mindenféle memóriakezelési probléma bemutatható. A helyzet az, hogy valós projektek is tele vannak egyedi fejlesztésű sztring osztályokkal.

A problémát az okozza, hogy maga a nyelv beépített típusként nem ismeri a sztringet. Létezik a karaktertömb – beépített
típusként –, de ez nem adható át értékként. Létezik a szabványosított sztring osztály, az std::string
– ez meg
valójában csak egy osztály, amely nem igazán része a nyelvnek. Ez kényelmetlenségeket okoz, és kezdők számára érthetetlen
helyzeteket teremt. Hogy magyarázzuk meg ezeket a furcsa kimeneteket?
#include <iostream>
#include <string>
int main() {
std::string h = "hello";
std::cout << h + "!"; // hello!
std::cout << "!" + h; // !hello
std::cout << "hello" + "!"; // compile error (???!?!)
std::cout << h + '!'; // hello!
std::cout << '!' + h; // !hello
std::cout << '!' + '!'; // 66 (??!?!)
}
Ez csak a szabványos sztring volt. Hab a tortán az összes többi keretrendszer saját sztring osztálya. Bármilyen alkalmazás- vagy grafikus keretrendszert használunk, nem ússzuk meg, hogy előbb-utóbb előkerüljenek ezek is: QString, wxString és a többiek.
Maradjunk most a szabványos karaktertömbnél és sztring osztálynál. A kétféle reprezentáció nemcsak kényelmetlen, hanem a hatékonyságot is rontja. Tegyük fel, hogy szeretnénk átadni egy függvénynek paraméterként egy sztringet, például egy megnyitandó fájl nevét.
Hogyan tesszük ezt? Első ötletünk a megszokott char const *
lehet: nullával lezárt karaktertömbre mutató pointer.
Ez azoknak kellemetlen, akiknek a hívás helyén egy std::string
objektumban van az információ:
void fajlt_beolvas(char const *fajlnev) {
// ...
}
int main() {
std::string fn;
std::cin >> fn;
fajlt_beolvas(fn.c_str()); // :(
}
A problémát az okozza, hogy az std::string
osztálynak nincsen char const *
konverziója. Ez jogos,
általában véve elég rossz ötlet beépített típusra implicit konverziót definiálni, mert szinte bármilyen műveletnél megtalálná a
beépített operátorokat a fordító. Ehelyett a sztring osztálynak .c_str()
tagfüggvénye van, amely az objektum által
kezelt memóriaterületre mutató pointert ad. Viszont kényelmetlen mindig kiírni a metódushívást a paraméterátadás helyén.
Próbálkozzunk meg akkor inkább egy std::string
átvételével!
void fajlt_beolvas(std::string const & fajlnev) {
// ...
}
int main() {
fajlt_beolvas("adat.txt"); // std::string("adat.txt");
}
Ilyenkor kisebb hatékonytalanságba futunk bele. Ha a hívónak nem sztringje van, hanem C-s karaktertömbje, akkor egy konverziót
kell végrehajtani: meghívni az std::string(char const *)
paraméterű konstruktorát. Bár ez megtörténik automatikusan,
de ez a konstruktor létrehozza a sztring másolatát dinamikus memóriaterületen – ami pedig teljesen felesleges volt.
A megoldást egy olyan osztály adja, amelyik:
- Automatikusan, konverzióval létrehozható – a nyers pointerrel ellentétben.
- Nem végez memóriakezelést – ahogy a nyers pointernél is ez teljesül.
Vegyük észre, hogy valójában az std::string
memóriakezelésére itt nincsen szükségünk. A függvény első változata, a
char const *
paraméterű, már rámutatott erre: amíg a függvény belsejében vagyunk, és nem térünk vissza a hívóhoz,
addig a hívó nem is fogja kivenni a pointer alól a karaktertömböt. Ezért nincsen szükség másolatra se. Egy olyan objektum kell csak,
amelyik hivatkozik egy karaktertömböt a memóriában, de nem ő maga kezeli annak élettartamát.
A keresett osztály az std::string_view
, amelyik a C++17 óta létezik. A legegyszerűbb használatához a következőket kell
tudni:
- Eltárol egy hivatkozást egy sztringhez, de nem másolja le azt. Tudja, hogy hol van egy karaktertömb eleje és vége
a memóriában, de nem ő foglalja, és nem is ő szabadítja fel azt. Úgy képzelhetjük el, mint egy szöveg referenciáját
– erre utal a
view
szó a nevében. - Van
std::string_view(char const *)
konstruktora, tehát képes „becsomagolni” egy nullával lezárt karaktertömböt. - Van
std::string::operator std::string_view()
konverzió is, tehát egy meglévő sztring objektumból is létrehozható. - Egyébiránt sztringként viselkedik, pl.
==
operátorral egyenlőségre vizsgálható,[]
operátorral indexelhető stb.
Ez kényelmesen és hatékonyan használható mindkét esetben:
#include <string_view>
void fajlt_beolvas(std::string_view fajlnev) {
// ...
}
int main() {
fajlt_beolvas("adat.txt"); // std::string_view("adat.txt"),
// de nem készül dinamikus másolat!
std::string fn;
std::cin >> fn;
fajlt_beolvas(fn); // fn.operator std::string_view(),
// és automatikus a konverzió!
}
A használatakor arra kell ügyelni, hogy a string_view
objektum ne élje túl a karaktertömböt,
azaz a hivatkozott sztring objektumot. Leginkább emiatt függvényparaméternek alkalmas ez a típus.
Az std::string_view
összehasonlító operátorokkal, iterátorokkal és egyéb tagfüggvényekkel is rendelkezik. Ezért
célravezetőbb lehet sok helyen ezt használni sima char const *
helyett.
#include <string_view>
#include <iostream>
int main() {
char s1[100], s2[100];
std::cin >> s1 >> s2;
if (std::string_view(s1) == std::string_view(s2)) { // operator==
std::cout << "egyforma" << std::endl;
}
int cnt = 0;
for (char c : std::string_view(s1)) // iterator
if (c == 'x')
++cnt;
std::cout << cnt << std::endl;
}
Ezek a példák persze kicsit sántítanak, mert char const *
konstruktorparaméter esetén a string_view
kénytelen a konstruktorban meghatározni a sztring hosszát – és az összehasonlításhoz, illetve a megszámláláshoz amúgy is
végig kellett volna menni a tömbön.
Vannak olyan sztringműveletek, amelyek részsztringeket állítanak elő. Például:
std::string::substr(pos, len)
– részsztring előállítása, mint alapművelet.- Egy sztring elejéről és végéről a szóközök levágása – ezt
trim()
-nek szoktuk nevezni. - Sztring darabolása –
split()
.
Ha ezekkel std::string
objektum(oka)t állítunk elő, akkor az lassú lehet, hiszen minden egyes részsztring
előállítása ilyenkor dinamikus memóriakezelést, tömbmásolást jelent. Viszont ha tudjuk, hogy a darabolt/vágott sztring megmarad
a memóriában, string
-ek helyett string_view
objektumokat is létrehozhatunk, amelyek csak hivatkoznak
a vágott részre.
Pl. a trim()
művelethez:
#include <iostream>
#include <string>
#include <string_view>
std::string_view trim(std::string_view s) {
/* szóközök az elején */
size_t begin = 0;
while (begin < s.length() && s[begin] == ' ')
++begin;
/* szóközök a végén */
size_t end = s.length();
while (end > begin && s[end - 1] == ' ')
--end;
return s.substr(begin, end - begin); // string_view::substr
}
int main() {
std::cout << "Írj be egy sort!\n";
std::string eredeti;
std::getline(std::cin, eredeti);
std::string_view vagott = trim(eredeti);
std::cout << '[' << vagott << ']';
}
Hasonlóképp működhetne a split()
is, amelyik std::vector<std::string_view>
objektumot állítana elő.