JavaRush /Курси /C++ SELF /Володіння чи представлення: std::string і std::string_vie...

Володіння чи представлення: std::string і std::string_view

C++ SELF
Рівень 18 , Лекція 0
Відкрита

1. Вступ

Якщо ви коли-небудь копіювали рядок «просто щоб передати його у функцію», знайте: ви не самотні. Часто так роблять за звичкою — доти, доки не починають уважно стежити за тим, хто насправді зберігає дані.

Сьогодні розберемо просте запитання: хто відповідає за життя символів у рядку. І зʼясуємо, чому інколи фраза «просто передати рядок» раптом означає «скопіювати пів роману».

Уявіть, що в нас є функція, яка «просто друкує рядок»:

#include <iostream>
#include <string>

void print_message(std::string s) {        // <- приймаємо за значенням (копія)
    std::cout << s << '\n';
}

int main() {
    std::string msg = "Hello";
    print_message(msg);
}

За змістом усе гаразд: ми передали текст, функція його вивела. Але фактично програма могла виконати зайву роботу: створити копію msg усередині print_message(). Іноді це не проблема, бо рядок короткий. А іноді витрати вже помітні — якщо рядок великий і таких викликів багато.

Тут і виникає слушна думка: якщо функція не збирається володіти рядком, то, можливо, копія їй і не потрібна. Їй потрібен лише доступ до символів.

2. std::string — власник даних

Щоб зрозуміти, чим std::string_view відрізняється від std::string, спершу варто як слід познайомитися зі std::string. Це не «просто масив символів типу char», а повноцінний обʼєкт із власною відповідальністю.

std::string володіє своїми символами: керує памʼяттю і відповідає за те, щоб дані існували, доки живий сам обʼєкт рядка. Так, інколи це означає виділення памʼяті та копіювання.

Якщо дуже спростити й не занурюватися в низькорівневі деталі, то в std::string є «метадані» і десь окремо — «буфер із символами». Усередині обʼєкт зберігає довжину, зазвичай також місткість, а ще — посилання або вказівник на місце, де зберігаються символи. Головне тут ось що: цей буфер належить рядку. Коли рядок знищується, зникають і його символи.

Дивіться на це як на коробку з речами. Поки коробка стоїть у вас удома, речі доступні. Прибрали коробку — доступу більше немає.

Ось невеликий приклад «життя рядка в межах блоку»:

#include <iostream>
#include <string>

int main() {
    std::string s = "Alive";
    std::cout << s << '\n';               // Alive
} // <- s знищується тут, разом із володінням даними

Є й ще одна важлива деталь, яку ви вже частково бачили на std::vector: інколи рядок змінюється так, що йому потрібно більше місця, і тоді він може «переїхати» в новий буфер. Це не помилка, а звичний механізм оптимізації. Ми поки не заглиблюємося в правила інвалідизації посилань і вказівників, але сам факт варто зафіксувати: власник може змінювати свій внутрішній буфер.

3. std::string_view — представлення даних

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

Це тип-представлення (view): він не володіє символами і зазвичай улаштований як «вказівник на символи + довжина». Якщо сказати зовсім прямо, std::string_view — це не рядок-власник, а «табличка з адресою та кількістю символів». Він не виділяє памʼять під текст і не копіює символи, коли ви створюєте його з уже наявних даних.

Найпростіший приклад:

#include <iostream>
#include <string>
#include <string_view>

int main() {
    std::string s = "Hello";
    std::string_view v = s;              // v дивиться на дані всередині s

    std::cout << s << '\n';              // Hello
    std::cout << v << '\n';              // Hello
}

Тут v — це представлення вмісту s. Воно легке, його дешево копіювати, і воно чудово підходить для сценарію «прочитати й забути».

Корисно порівняти std::string і std::string_view у вигляді таблиці:

Властивість
std::string
std::string_view
Володіє символами Так Ні
Може зберігати результат «назавжди» Так (доки обʼєкт живий) Ні (лише доки живі чужі дані)
Створення з
std::string
Може копіювати Зазвичай не копіює
Підходить для зміни тексту Так Ні (це доступ лише для читання)
Типовий сценарій «Я зберігаю текст» «Я читаю текст»

У стандартній бібліотеці ви зустрінете багато місць, де інтерфейси спеціально приймають std::string_view, щоб уникати зайвих копій і робити API гнучкішим.

Як view «дивиться» на власника

Уявімо, що є один текст і два обʼєкти поруч. Один обʼєкт — власник (std::string), другий — спостерігач (std::string_view). Спостерігач не робить копії і не стає «другим власником»: він просто запамʼятовує, де лежать символи власника і скільки їх потрібно враховувати.

Ось схема, яка допомагає не заплутатися:

flowchart LR
    S[std::string s
володіє даними] --> B[(буфер символів)] V[std::string_view v
не володіє] --> B

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

Є й цікавий ефект: якщо ви зміните символ у std::string, view «побачить» цю зміну, бо дивиться на ті самі дані:

#include <iostream>
#include <string>
#include <string_view>

int main() {
    std::string s = "hello";
    std::string_view v = s;

    s[0] = 'H';
    std::cout << s << '\n';              // Hello
    std::cout << v << '\n';              // Hello
}

Звучить зручно, але тут уже видно і майбутні ризики: view не захищає вас від дій власника. Якщо власник вирішить, що йому потрібен новий буфер, наприклад коли рядок сильно виросте, view може раптом перетворитися на «те, що дивиться за старою адресою». Базове правило тут просте: view живе за правилами власника, а не навпаки.

Що вміє і чого не вміє std::string_view

На цьому етапі виникає природне бажання: «О, клас! Давайте тепер замість std::string завжди писати std::string_view — і буде безплатно й швидко». Це приблизно як вирішити харчуватися лише соусом, бо він смачний. Соус хороший, але їжу не замінює.

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

Невеликий приклад базових операцій:

#include <iostream>
#include <string_view>

int main() {
    std::string_view v = "abc";

    std::cout << v.size() << '\n';       // 3
    std::cout << v[0] << '\n';           // a
}

Якщо вам потрібно отримати власну копію, наприклад щоб зберегти результат надовго, ви повертаєтеся до std::string:

#include <iostream>
#include <string>
#include <string_view>

int main() {
    std::string_view v = "Hi";
    std::string owned(v);                // копіюємо символи

    std::cout << owned << '\n';          // Hi
}

Є ще один нюанс, який новачки часто пропускають: string_view — це «вказівник + довжина», а не обовʼязково C-рядок із '\0' наприкінці. Він може дивитися на фрагмент усередині рядка, на частину масиву символів і взагалі не зобовʼязаний бути нуль-термінованим у звичному сенсі.

Тому думка «зараз візьму v.data() — і все всюди працюватиме як C-рядок» небезпечна. Ми не заглиблюємося тут у C-API, просто запамʼятаймо: view обіцяє швидкий доступ до діапазону символів, але не обіцяє зручності «як у C-рядка».

4. Приклад: команда без копій у консольному застосунку

Щоб тема не залишилася «теорією про вказівники», привʼяжімо її до навчального консольного застосунку. Нехай це буде простий мініінтерпретатор команд: програма читає рядок, а потім вирішує, що робити — наприклад, вийти, показати довідку чи сказати, що команда невідома.

Сьогодні мета скромна: навчитися «дивитися» на введений рядок без зайвих копій.

Важливо: ми не робимо тут складний парсинг і не розбиваємо рядок на аргументи. Тут ми тренуємо базову ідею: «є власник рядка, є view на нього».

#include <iostream>
#include <string>
#include <string_view>

int main() {
    std::string line;

    while (std::getline(std::cin, line)) {
        std::string_view sv = line; // sv живе тільки всередині ітерації

        if (sv == "exit") {
            std::cout << "Bye!\n";  // Bye!
            break;
        }

        if (sv == "help") {
            std::cout << "Commands: help, exit\n"; // Commands: help, exit
            continue;
        }

        std::cout << "Unknown: " << sv << '\n';    // Unknown: ...
    }
}

Зверніть увагу на підхід: std::string_view sv = line; створюється поруч із місцем використання і живе недовго — рівно стільки, скільки живе line у цій ітерації циклу. Це саме той природний сценарій, для якого string_view і придумали: швидко подивитися, порівняти, ухвалити рішення.

Якщо ви зараз думаєте: «А чи можна передавати std::string_view у функції замість const std::string&?» — так, і це наступний логічний крок, але не забігаймо наперед. Тут важливо, щоб мозок чітко розділив ролі: line зберігає, sv дивиться.

5. Типові помилки під час роботи з std::string_view

Помилка № 1: вважати, що std::string_view зберігає копію тексту.
Це найпоширеніша пастка: ви бачите «рядковий» тип, він друкується через std::cout, порівнюється, у нього є size() — і мозок автоматично думає: «Ну це ж рядок». Але це не рядок-власник. Це лише «ярличок» на чужі символи. Тому якщо власник зник, view не стає магічно коректним: він перетворюється на дуже впевнений вказівник у нікуди.

Помилка № 2: повертати std::string_view на локальний std::string.
Іноді хочеться зробити «зручну» функцію, яка повертає view, бо це «швидко». Але якщо всередині функції ви створили локальний рядок, то після виходу з неї рядок знищиться. View залишиться, а даних уже не буде. Навіть якщо на маленьких тестах це «ніби працює», це саме той випадок, коли програма просто ще не встигла вас підставити.

#include <string>
#include <string_view>

std::string_view bad() {
    std::string s = "local";
    return std::string_view{s}; // s помре після return
}

Помилка № 3: намагатися «редагувати» текст через view.
std::string_view за своєю суттю не призначений для редагування. Він не дає вам push_back(), +=, replace() та інших радощів будівельної бригади. Якщо потрібно змінювати текст, змінюйте власника (std::string) або створюйте новий std::string і володійте результатом.

Помилка № 4: звертатися до operator[] без перевірки меж.
У std::string_view є operator[], і він не перевіряє межі — так само, як і в std::string. Тому вираз sv[0] для порожнього рядка — це прямий квиток у невизначену поведінку. Якщо індекс приходить ззовні, наприклад від користувача, спочатку перевіряйте empty() і size().

Помилка № 5: робити view «довгоживучим» за звичкою.
Новачки інколи оголошують std::string_view current; «десь нагорі», присвоюють туди то один рядок, то інший, а потім дивуються, чому за кілька кроків там сміття. View хороший, коли живе недовго: створили → використали → забули. Чим довше живе view, тим складніше гарантувати, що власник іще живий і що його буфер не змінився.

Коментарі
ЩОБ ПОДИВИТИСЯ ВСІ КОМЕНТАРІ АБО ЗАЛИШИТИ КОМЕНТАР,
ПЕРЕЙДІТЬ В ПОВНУ ВЕРСІЮ