C++

std::string_view: невласницький погляд на рядок без копіювання

Клас std::string_view (C++17): внутрішня модель {ptr, length}, створення з const char* та std::string, методи read-only доступу, remove_prefix/remove_suffix, відсутність нуль-термінатора. Dangling view — найнебезпечніша пастка. Коли обирати string_view, а коли const std::string&.

std::string_view: погляд без копіювання

Один невинний виклик функції — і три копії рядка

Розгляньте цю функцію та подумайте, скільки разів рядок "Hello, World!" буде скопійований під час одного виклику:

CopyPuzzle.cpp
#include <iostream>
#include <string>

bool startsWith(const std::string& s, const std::string& prefix)
{
    return s.substr(0, prefix.length()) == prefix;
}

int main()
{
    std::cout << std::boolalpha;
    std::cout << startsWith("Hello, World!", "Hello") << "\n";

    return 0;
}
./CopyPuzzle
$ ./CopyPuzzle
true
Execution finished with exit code 0.

Відповідь: щонайменше три копії.

  1. "Hello, World!" — C-рядковий літерал. Компілятор неявно конструює з нього тимчасовий об'єкт std::string для параметра s. Якщо довжина перевищує поріг SSO (~15 символів на GCC) — відбувається виділення пам'яті на купі.
  2. "Hello" — аналогічно, тимчасовий std::string для параметра prefix.
  3. s.substr(0, prefix.length()) — ще один тимчасовий std::string з перших 5 символів.

Зрештою, три виділення пам'яті, три копіювання, три звільнення — лише щоб перевірити рівність двох незмінних рядків. Це є системною проблемою рядкових API у C++, і std::string_view, доданий у C++17, вирішує її елегантно та повністю.


Проблема: незмінні рядки не потребують копіювання

Щоб краще зрозуміти мотивацію, розглянемо розгорнутий приклад із реальної практики:

TheCopyCost.cpp
#include <iostream>
#include <string>

// Ця функція лише читає рядок — вона нічого не змінює.
// Але через const std::string& вона все одно змушує компілятор
// створювати тимчасовий об'єкт при передачі C-рядка.
bool isEmail(const std::string& s)
{
    size_t at  = s.find('@');
    size_t dot = s.rfind('.');

    return at != std::string::npos
        && dot != std::string::npos
        && at < dot
        && at > 0
        && dot < s.length() - 1;
}

int main()
{
    // Варіант 1: передаємо C-рядковий літерал
    // → компілятор створює тимчасовий std::string("user@example.com")
    // → виділення пам'яті на купі (17 символів > SSO 15)
    std::cout << isEmail("user@example.com") << "\n";  // true

    // Варіант 2: передаємо std::string — копіювання через & не відбувається, але
    // якщо б параметр був не посиланням, а значенням (std::string s) — копія неминуча
    std::string addr = "admin@company.org";
    std::cout << isEmail(addr) << "\n"; // true

    // Варіант 3: у нас є char* з зовнішнього API
    char buf[] = "noreply@mail.ua";
    std::cout << isEmail(buf) << "\n"; // true — ще одна тимчасова копія

    return 0;
}

У всіх трьох викликах функція isEmail лише читає рядок. Жодної модифікації. Але через те, що параметр оголошено як const std::string&, передача const char* або char[] вимагає неявної конструкції тимчасового об'єкта std::string. Цього накладного витрату можна повністю уникнути.


Що таке std::string_view

std::string_view — це клас зі стандартної бібліотеки C++17, що зберігає пару значень:

{ const char* ptr,  size_t length }

Він не виділяє пам'яті. Він не копіює жодного символу. Він лише вказує на вже існуючий масив символів десь в пам'яті та запам'ятовує, скільки з них потрібно враховувати.

Loading diagram...
@startuml
skinparam style plain
skinparam defaultFontName "JetBrains Mono"
skinparam backgroundColor transparent
skinparam defaultFontSize 13

title Внутрішня модель std::string_view

package "std::string_view (16 байтів)" as sv #3b82f6 {
  rectangle "const char* ptr" as ptr #2563eb
  rectangle "size_t length = 5" as len #2563eb
}

package "Пам'ять (рядковий літерал / char[] / heap)" as mem #475569 {
  rectangle "'H' 'e' 'l' 'l' 'o' ',' ' ' 'W' 'o' 'r' 'l' 'd' '!' '\\0'" as buf #334155
}

ptr -right-> buf : вказує на позицію 0

note bottom of sv
  Не є власником даних.
  Не виділяє пам'яті.
  Не звільняє пам'яті.
end note

@enduml

Аналогія, яку варто запам'ятати: std::string_view — це вікно у чужу кімнату. Вікно дозволяє дивитись і описувати те, що бачиш, — але не дозволяє нічого переставити всередині. І якщо кімнату зруйнують, вікно стане оглядовим майданчиком в порожнечу.


Підключення та основні властивості

std::string_view знаходиться у заголовку <string_view> (C++17):

BasicView.cpp
#include <iostream>
#include <string>
#include <string_view>

int main()
{
    std::string_view sv1 = "Hello, World!"; // з рядкового літерала
    std::string       s  = "Hello, World!";
    std::string_view sv2 = s;              // з std::string

    // Базові запити — ті самі, що й у std::string
    std::cout << "length:  " << sv1.length()  << "\n"; // 13
    std::cout << "empty:   " << std::boolalpha << sv1.empty() << "\n"; // false
    std::cout << "front:   " << sv1.front()   << "\n"; // H
    std::cout << "back:    " << sv1.back()    << "\n"; // !
    std::cout << "sv1[4]:  " << sv1[4]        << "\n"; // o

    // Розмір самого об'єкта — лише ptr + length
    std::cout << "sizeof(sv1): " << sizeof(sv1) << "\n"; // 16 (на 64-bit)
    std::cout << "sizeof(s):   " << sizeof(s)   << "\n"; // 32

    return 0;
}
./BasicView
$ ./BasicView
length: 13
empty: false
front: H
back: !
sv1[4]: o
sizeof(sv1): 16
sizeof(s): 32
Execution finished with exit code 0.

Об'єкт std::string_view займає рівно 16 байтів (на 64-бітній системі): 8 байтів для вказівника та 8 байтів для довжини. Це незалежно від довжини рядка, на який він вказує.


Способи створення std::string_view

std::string_view можна сконструювати з трьох джерел:

Construction.cpp
#include <iostream>
#include <string>
#include <string_view>

int main()
{
    // 1. З рядкового літерала (найпоширеніше)
    std::string_view sv1 = "Hello";
    std::cout << sv1 << "\n"; // Hello

    // 2. З об'єкта std::string (неявна конвертація — дозволена)
    std::string str = "World";
    std::string_view sv2 = str;
    std::cout << sv2 << "\n"; // World

    // 3. З char* та явної довжини — для не-нуль-термінованих буферів
    char vowels[] = {'a', 'e', 'i', 'o', 'u'}; // немає '\0'!
    std::string_view sv3(vowels, 5);
    std::cout << sv3 << "\n"; // aeiou

    // 4. З char[] — вся довжина автоматично
    char buf[] = "C++ is great";
    std::string_view sv4 = buf;
    std::cout << sv4 << "\n"; // C++ is great

    // 5. Копіювання string_view — не копіює дані, лише ptr+length
    std::string_view sv5 = sv1; // sv5 вказує на той самий літерал "Hello"
    std::cout << sv5 << "\n"; // Hello

    return 0;
}
./Construction
$ ./Construction
Hello
World
aeiou
C++ is great
Hello
Execution finished with exit code 0.
Варіант 3 — конструктор string_view(ptr, length) — є унікальною перевагою перед std::string. Він дозволяє «дивитись» на масив символів, що не має нуль-термінатора: бінарні дані, мережеві буфери, результати роботи парсерів.

Ключові правила конвертації:

НапрямДозволеноЯк
std::stringstd::string_view✅ неявноstd::string_view sv = myStr;
const char*std::string_view✅ неявноstd::string_view sv = "text";
std::string_viewstd::string⚠️ лише явноstd::string s(sv); або std::string s{sv};
std::string_viewconst char*❌ напрямуПотрібен std::string + .c_str()

Функціональність read-only: що доступно

std::string_view підтримує майже весь «читаючий» API std::string. Пошукові методи, доступ за індексом, порівняння — все це є:

ReadonlyAPI.cpp
#include <iostream>
#include <string_view>

int main()
{
    std::string_view sv = "Hello, World! 12345";

    // Доступ до символів
    std::cout << sv[0]     << "\n"; // H
    std::cout << sv.at(1)  << "\n"; // e
    std::cout << sv.front()<< "\n"; // H
    std::cout << sv.back() << "\n"; // 5

    // Метрики
    std::cout << sv.length() << "\n"; // 19
    std::cout << sv.empty()  << "\n"; // 0 (false)

    // Пошук — ті самі методи, що у std::string
    size_t comma = sv.find(',');
    std::cout << "Кома на позиції: " << comma << "\n"; // 5

    size_t firstDigit = sv.find_first_of("0123456789");
    std::cout << "Перша цифра на: " << firstDigit << "\n"; // 14

    // substr — повертає string_view (не std::string!), без копіювання
    std::string_view word = sv.substr(7, 5);
    std::cout << "Слово: " << word << "\n"; // World

    // Порівняння
    std::cout << std::boolalpha;
    std::cout << (sv == "Hello, World! 12345") << "\n"; // true
    std::cout << sv.starts_with("Hello")       << "\n"; // true (C++20)
    std::cout << sv.ends_with("12345")         << "\n"; // true (C++20)

    return 0;
}
./ReadonlyAPI
$ ./ReadonlyAPI
H
e
H
5
19
false
Кома на позиції: 5
Перша цифра на: 14
Слово: World
true
true
true
Execution finished with exit code 0.

Зверніть увагу на критичну відмінність у методі .substr():

  • std::string::substr() → повертає std::string (нову копію)
  • std::string_view::substr() → повертає std::string_view (нове вікно, нуль копій)

Це означає, що ланцюжок операцій з string_view::substr() не виділяє пам'яті взагалі:

std::string_view sv   = "  Hello, World!  ";
std::string_view word = sv.substr(2, 5); // "Hello" — без копіювання

Звуження вікна: remove_prefix та remove_suffix

std::string_view надає два унікальні методи для переміщення меж вікна без зміни вихідних даних:

  • .remove_prefix(n) — пересуває початок вікна на n символів вправо;
  • .remove_suffix(n) — скорочує кінець вікна на n символів.
RemovePrefixSuffix.cpp
#include <iostream>
#include <string_view>

int main()
{
    std::string_view sv = ">>>Hello<<<";
    std::cout << sv << "\n"; // >>>Hello<<<

    sv.remove_prefix(3); // відсунути початок на 3
    std::cout << sv << "\n"; // Hello<<<

    sv.remove_suffix(3); // скоротити кінець на 3
    std::cout << sv << "\n"; // Hello

    // Практичний приклад: trim без виділення пам'яті
    std::string_view padded = "   Hello, World!   ";

    size_t start = padded.find_first_not_of(" \t");
    size_t end   = padded.find_last_not_of(" \t");

    // Замість substr + копіювання — звужуємо вікно
    padded.remove_prefix(start);
    padded.remove_suffix(padded.length() - end - 1);

    std::cout << "'" << padded << "'\n"; // 'Hello, World!'

    return 0;
}
./RemovePrefixSuffix
$ ./RemovePrefixSuffix
>>>Hello<<<
Hello<<<
Hello
'Hello, World!'
Execution finished with exit code 0.
Операції remove_prefix та remove_suffixнезворотні. Після виклику sv.remove_prefix(3) повернути перші 3 символи неможливо — вікно назад не відкривається. Якщо потрібна можливість повернення до початкового стану, збережіть копію string_view перед звуженням.

Порівняємо ефективність двох підходів до trim:

Loading diagram...
@startuml
skinparam style plain
skinparam defaultFontName "JetBrains Mono"
skinparam backgroundColor transparent
skinparam defaultFontSize 13

title trim: std::string::substr() vs string_view

package "Через std::string::substr()" #ef4444 {
  rectangle "1. find_first_not_of → start" as s1 #dc2626
  rectangle "2. find_last_not_of → end" as s2 #dc2626
  rectangle "3. substr(start, end-start+1)" as s3 #dc2626
  rectangle "4. new char[n] — виділення купи" as s4 #dc2626
  rectangle "5. memcpy — копіювання n символів" as s5 #dc2626
  rectangle "6. Повернення std::string (власник)" as s6 #dc2626
  s1 --> s2 --> s3 --> s4 --> s5 --> s6
}

package "Через string_view" #22c55e {
  rectangle "1. find_first_not_of → start" as v1 #16a34a
  rectangle "2. find_last_not_of → end" as v2 #16a34a
  rectangle "3. remove_prefix + remove_suffix" as v3 #16a34a
  rectangle "4. Повернення string_view (ptr+len)" as v4 #16a34a
  v1 --> v2 --> v3 --> v4
}

note right of s4
  O(n) виділення + копіювання
end note

note right of v3
  O(1) — лише зміна ptr і len
end note

@enduml

Що std::string_view НЕ вміє

std::string_view — виключно читаючий тип. Будь-яка операція, що вимагає зміни вмісту рядка, відсутня:

Метод std::stringДоступний у string_view?
operator[], at(), front(), back() (читання)
length(), size(), empty()
find(), rfind(), find_first_of() тощо
substr() (повертає string_view)
starts_with(), ends_with() (C++20)
remove_prefix(), remove_suffix()✅ (звужують вікно)
data() (вказівник без гарантії '\0')
append(), push_back(), +=❌ немодифікований
insert(), erase(), replace()
resize(), reserve(), clear()
c_str() (з гарантією '\0')
Конструювання з числа

Відсутність нуль-термінатора: пастка з .data()

std::string_view зберігає пару {ptr, length} і не гарантує наявності '\0' після останнього символу. Це має критичне значення при взаємодії з C-функціями, що очікують нуль-термінований рядок:

DataPitfall.cpp
#include <iostream>
#include <string_view>
#include <cstring> // strlen

int main()
{
    // Безпечний випадок: sv вказує на літерал, який має '\0'
    std::string_view sv1 = "Hello";
    std::cout << std::strlen(sv1.data()) << "\n"; // 5 — ОК, бо '\0' є

    // Небезпечний випадок: після remove_prefix '\0' вже не на правильному місці
    std::string_view sv2 = "Hello, World!";
    sv2.remove_prefix(7); // sv2 = "World!" — але data() вказує на 'W', '\0' є в кінці
    std::cout << std::strlen(sv2.data()) << "\n"; // 6 — ОК в цьому конкретному випадку

    // СПРАВДІ небезпечний випадок: substr з масиву без '\0'
    char buf[] = {'H', 'e', 'l', 'l', 'o', 'X', 'Y', 'Z'};
    std::string_view sv3(buf, 5); // "Hello" — length=5, але НЕМАЄ '\0' після 'o'!
    // НЕБЕЗПЕЧНО: strlen читатиме 'X', 'Y', 'Z' і далі, поки не знайде '\0'
    // std::cout << std::strlen(sv3.data()); // UB — читання за межами

    std::cout << "sv3 через cout: " << sv3 << "\n"; // OK — cout знає length
    std::cout << "sv3 length:     " << sv3.length() << "\n"; // 5

    return 0;
}
./DataPitfall
$ ./DataPitfall
5
6
sv3 через cout: Hello
sv3 length: 5
Execution finished with exit code 0.
Ніколи не передавайте sv.data() у функції, що очікують нуль-термінований рядок (strlen, printf, fopen, будь-яке C API), якщо ви не повністю впевнені, що '\0' знаходиться одразу після sv.length() символів. Правильна процедура: std::string tmp(sv); func(tmp.c_str());

Данглінг-вигляд: найнебезпечніша пастка

Найпідступніша помилка при роботі з std::string_viewdangling view: ситуація, коли вигляд продовжує існувати після знищення рядка, на який він вказував.

Пастка 1: повернення вигляду з функції

Dangling1.cpp
#include <iostream>
#include <string>
#include <string_view>

std::string_view dangerousFunction()
{
    std::string local = "I am local"; // об'єкт на стеку функції
    return local; // ПОМИЛКА: повертаємо string_view на локальний std::string,
                  // який буде знищений при виході з функції!
}

int main()
{
    std::string_view sv = dangerousFunction();
    // sv тепер вказує на звільнену пам'ять!
    // Будь-яке звернення до sv — невизначена поведінка (UB)
    std::cout << sv << "\n"; // UB: може вивести сміття, завершитись із помилкою, "спрацювати"
    return 0;
}
./Dangling1 (приклад UB)
$ ./Dangling1
�P@�P@ (сміття — типовий прояв UB)
або segmentation fault, або «правильний» вивід — непередбачувано

Пастка 2: тимчасовий об'єкт на одному рядку

Dangling2.cpp
#include <iostream>
#include <string>
#include <string_view>

int main()
{
    // ПОМИЛКА: std::string("temporary") — тимчасовий об'єкт
    // Він буде знищений в кінці цього оператора присвоювання.
    // sv продовжить вказувати на звільнену пам'ять.
    std::string_view sv = std::string("temporary"); // UB!

    std::cout << sv << "\n"; // UB

    return 0;
}

Пастка 3: модифікація рядка-власника

Dangling3.cpp
#include <iostream>
#include <string>
#include <string_view>

int main()
{
    std::string s = "Hello";
    std::string_view sv = s; // sv вказує на внутрішній буфер s

    std::cout << sv << "\n"; // Hello — ОК

    // Модифікуємо рядок: якщо відбудеться перевиділення пам'яті,
    // sv стане dangling view!
    s += " World, this is a long string that exceeds SSO boundary!";
    // s перевиділила буфер — старий буфер звільнено.
    // sv.data() тепер вказує на звільнену пам'ять.

    std::cout << sv << "\n"; // UB: dangling view
    return 0;
}
Loading diagram...
@startuml
skinparam style plain
skinparam defaultFontName "JetBrains Mono"
skinparam backgroundColor transparent
skinparam defaultFontSize 13

title Dangling view після перевиділення std::string

rectangle "До s +=" as before #22c55e {
  rectangle "s.ptr ──►" as sptr1 #16a34a
  rectangle "s.len = 5" as slen1 #16a34a
  rectangle "sv.ptr ──►" as svptr1 #3b82f6
  rectangle "sv.len = 5" as svlen1 #3b82f6
}

rectangle "Купа (buffer A)" as heapA #166534 {
  rectangle "'H''e''l''l''o''\\0'" as bufA #14532d
}

rectangle "Після s +=" as after #ef4444 {
  rectangle "s.ptr ──►" as sptr2 #dc2626
  rectangle "s.len = 57" as slen2 #dc2626
  rectangle "sv.ptr ──► (DANGLING!)" as svptr2 #7c3aed
  rectangle "sv.len = 5" as svlen2 #7c3aed
}

rectangle "Купа (buffer B, новий)" as heapB #166534 {
  rectangle "'H''e''l''l''o'' ''W''o''r''l''d''...''\\0'" as bufB #14532d
}

rectangle "Купа (buffer A, ЗВІЛЬНЕНО)" as heapDead #991b1b {
  rectangle "??? (freed memory)" as bufDead #7f1d1d
}

before -right-> heapA
sptr1 -right-> bufA
svptr1 -right-> bufA
sptr2 -right-> bufB
svptr2 -down-> bufDead

@enduml
Три правила безпечного використання std::string_view:
  1. Не зберігайте string_view у полях класів або глобальних змінних — важко гарантувати час життя.
  2. Не повертайте string_view з функцій (крім випадку, коли він посилається на один зі своїх аргументів-string_view).
  3. Не створюйте string_view від тимчасового std::string — тимчасовий об'єкт живе лише до кінця виразу. ::

Перевизначення функцій: виправлення прикладу з початку статті

Повернемося до функції з hook і перепишемо її правильно:
FixedStartsWith.cpp
#include <iostream>
#include <string>
#include <string_view>

// До C++17: дві потенційні копії при передачі C-рядків
bool startsWithOld(const std::string& s, const std::string& prefix)
{
    return s.substr(0, prefix.length()) == prefix; // + копія substr
}

// C++17: нуль копій, нуль виділень пам'яті
bool startsWith(std::string_view s, std::string_view prefix)
{
    if (prefix.length() > s.length()) return false;
    return s.substr(0, prefix.length()) == prefix; // substr повертає string_view!
}

// Або ще краще — через starts_with (C++20):
bool startsWithModern(std::string_view s, std::string_view prefix)
{
    return s.starts_with(prefix);
}

bool isEmail(std::string_view s)
{
    size_t at  = s.find('@');
    size_t dot = s.rfind('.');
    return at != std::string_view::npos
        && dot != std::string_view::npos
        && at < dot
        && at > 0
        && dot < s.length() - 1;
}

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

    // Усі виклики — без будь-якого виділення пам'яті:
    std::cout << startsWith("Hello, World!", "Hello")   << "\n"; // true
    std::cout << startsWith("Hello, World!", "Goodbye") << "\n"; // false

    std::string s = "Hello, World!";
    std::cout << startsWith(s, "Hello")                 << "\n"; // true

    std::cout << isEmail("user@example.com")             << "\n"; // true
    std::cout << isEmail("notanemail")                   << "\n"; // false

    return 0;
}
./FixedStartsWith
$ ./FixedStartsWith
true
false
true
true
false
Execution finished with exit code 0.
Функція з параметром std::string_view приймає без копіювання:
  • const char* та рядкові літерали
  • std::string (через неявну конвертацію)
  • інші std::string_view
  • char[] будь-якої довжини

Конвертація string_viewstd::string

Коли потрібно отримати справжній std::string з string_view (наприклад, щоб зберегти, передати в C-API або модифікувати), конверсія виконується явно:
Conversion.cpp
#include <iostream>
#include <string>
#include <string_view>
#include <cstring>

void legacyCFunction(const char* str)
{
    std::cout << "C func: " << str << " (len=" << std::strlen(str) << ")\n";
}

int main()
{
    std::string_view sv = "Hello, World!";

    // 1. Явний конструктор std::string
    std::string s1(sv);
    std::cout << s1 << "\n"; // Hello, World!

    // 2. static_cast
    std::string s2 = static_cast<std::string>(sv);
    std::cout << s2 << "\n"; // Hello, World!

    // 3. Передача в C API: спочатку → std::string, потім → c_str()
    std::string tmp(sv);
    legacyCFunction(tmp.c_str()); // безпечно, '\0' гарантований

    // НЕПРАВИЛЬНО: sv.data() не гарантує '\0'
    // legacyCFunction(sv.data()); // потенційно небезпечно

    // 4. Неявна конвертація — ЗАБОРОНЕНА
    // std::string s3 = sv; // помилка компіляції
    // void func(std::string) { ... }
    // func(sv); // помилка компіляції

    return 0;
}
./Conversion
$ ./Conversion
Hello, World!
Hello, World!
C func: Hello, World! (len=13)
Execution finished with exit code 0.
Відсутність неявної конвертації string_viewstd::string — це навмисне рішення стандарту. Якби конвертація відбувалась неявно, переваги від string_view могли б непомітно нівелюватись: компілятор мовчки створював би копії скрізь, де функції чекають std::string.

Коли що використовувати: таблиця прийняття рішень

std::string_view
параметр функції
Функція лише читає рядок і не зберігає його після повернення. Приймає C-рядки, std::string та інші string_view без виділення пам'яті. Ідеальна заміна const std::string& для read-only функцій.
const std::string&
параметр функції
Функція лише читає, але передає далі в API, що вимагає const std::string& (наприклад, зберігає у контейнер типу std::map<std::string, ...>). Або якщо гарантовано передаватимуть лише std::string.
std::string
параметр функції (за значенням)
Функція зберігає або модифікує рядок. Семантика переміщення дозволяє ефективно передавати rvalue: func(std::move(s)).
std::string_view
локальна змінна
Посилання на підрядок без копіювання (замість substr()). Або зменшення «вікна» через remove_prefix/remove_suffix. Переконайтеся, що оригінал живе довше.
std::string_view
ЗАБОРОНЕНО як поле класу
Зберігання string_view у полях класу небезпечно: важко гарантувати, що рядок-власник проживе довше за об'єкт класу. Виняток — якщо клас задокументовано як невласницький view і гарантії надаються ззовні.
std::string_view
ЗАБОРОНЕНО як return у більшості випадків
Повертати string_view безпечно лише якщо він посилається на один із параметрів-string_view функції або на константний статичний буфер. Повертати view на локальний std::string — завжди UB.

::


Практика

Рівень 1 — Рефакторинг функцій на string_view

Перепишіть наступні функції, замінивши const std::string& на std::string_view де це доречно, та поясніть у коментарях чому.

Рівень 2 — trimView без копіювання

Реалізуйте функцію trimView(std::string_view sv), що повертає std::string_view без пробільних символів на початку і в кінці, не копіюючи жодного символу. Потім напишіть функцію splitView, що розбиває string_view за роздільником і повертає вектор string_view (усі елементи вказують у вихідний рядок).

Рівень 3 — Парсер HTTP-запиту

Напишіть функцію parseRequestLine, що розбирає перший рядок HTTP-запиту ("GET /index.html HTTP/1.1") на метод, шлях та версію — виключно через string_view без жодного виділення пам'яті. Результат — структура з трьома полями string_view.


Резюме

Що таке string_view

std::string_view (C++17) — це пара {const char* ptr, size_t length}. Об'єкт розміром 16 байтів, що не виділяє пам'яті і не копіює символів. Він лише вказує на вже існуючий масив і запам'ятовує, скільки символів враховувати.

Переваги

Приймає const char*, std::string, char[], інші string_viewбез копіювання. .substr() повертає string_view (нуль копій). remove_prefix/remove_suffix звужують вікно за O(1). Ідеальний параметр для read-only функцій.

Обмеження

Повністю read-only: немає append, insert, erase, replace, resize. Немає c_str() з гарантованим '\0'. Не конвертується неявно в std::string. Для передачі в C API потрібна проміжна std::string.

Dangling view

Найнебезпечніша пастка: якщо рядок-власник знищено або перевиділив буфер — string_view стає висячим вказівником. Не зберігайте string_view у полях класів. Не повертайте string_view на локальний std::string. Не прив'язуйте до тимчасових об'єктів.

Конвертація

std::stringstring_view: неявна, завжди безпечна. string_viewstd::string: лише явно — std::string s(sv). string_viewconst char*: через проміжний std::string + .c_str(). Заборона неявної конвертації — захист від непомітного копіювання.

Правило вибору

Функція лише читає рядок — параметр std::string_view. Функція зберігає рядок у структурі — const std::string& або std::string. Функція модифікує рядок — std::string& або std::string. Поле класу, що зберігає рядок — завжди std::string.

На цьому завершується модуль «Рядки у C++». За вісім статей ми пройшли шлях від char та ASCII-таблиці, через нуль-термінованість C-style рядків та їх небезпеки, до повного освоєння std::string — з моделлю пам'яті, SSO, управлінням ємністю, всіма методами модифікації та пошуку — і нарешті до std::string_view: інструмента, що дозволяє працювати з рядками в «режимі читання» без жодного копіювання.

Що далі? Наступний модуль — std::vector: динамічний масив загального призначення, що застосовує ті самі принципи управління пам'яттю (capacity, reserve, SSO-аналог для малих об'єктів), але для довільних типів даних, а не лише для символів.

Copyright © 2026