Урок №83. Клас std::string_view

  Юрій  | 

  Оновл. 4 Жов 2020  | 

 372

На цьому уроці ми розглянемо клас std::string_view, який є нововведенням стандарту С++17.

Проблеми рядків

На уроці про рядки C-style ми говорили про небезпеки, які виникають при їх використанні. Звичайно, рядки C-style працюють швидко, але при цьому їх використання не є таким вже й простим і безпечним в порівнянні з std::string.

Правда, варто відзначити, що і у std::string є свої недоліки, особливо при використанні константних рядків.

Розглянемо наступний приклад:

Результат виконання програми:

hello hello hello

Усередині функції main() виконується копіювання рядка hello 3 рази, в результаті чого ми отримуємо 4 копії вихідного рядка:

   перша копія — це безпосередньо сам рядковий літерал hello, який створюється на етапі компіляції і зберігається у бінарному вигляді;

   ще одна копія створюється при ініціалізації масиву типу char;

   далі йдуть об’єкти str і more класа std::string, кожен з яких, в свою чергу, створює ще по одній копії рядка.

Через те, що клас std::string спроектований так, щоб його об’єкти могли бути модифіковані, кожному об’єкту класу std::string доводиться зберігати свою власну копію рядка. Завдяки цьому, початковий рядок може бути змінений без впливу на інші об’єкти std::string.

Це також справедливо і для константних рядків (const std::string), незважаючи на те, що подібні об’єкти не можуть бути змінені.

Клас std::string_view

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

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

На відміну від об’єктів класу std::string, які зберігають свою власну копію рядка, клас std::string_view забезпечує представлення (англ. “view”) для заданого рядка, який може бути визначений де-небудь в іншому місці.

Спробуємо переписати код з попереднього прикладу, замінивши кожне входження std::string на std::string_view:

В результаті ми отримаємо такий самий результат, як і в попередньому прикладі, але при цьому у нас не будуть створені зайві копії рядка hello. Коли ми копіюємо об’єкт класу std::string_view, то новий об’єкт std::string_view буде “дивитися” на той же рядок, на який “дивився” початковий об’єкт. Крім того, клас std::string_view не тільки швидкий, але і володіє багатьма функціями, які ми розглядали при роботі з класом std::string:

Оскільки об’єкт класу std::string_view не створює копії рядка, то, змінивши початковий рядок, ми, тим самим, вплинемо і на його представлення у зв’язаному з ним об’єктом std::string_view:

Змінюючи arr, можна бачити, як змінюється і str. Це відбувається через те, що початковий рядок є спільним для цих змінних. Варто відзначити, що при використанні об’єктів класу std::string_view краще уникати модифікування вихідного рядка, поки існують пов’язані з ним об’єкти класу std::string_view, інакше це може призвести до плутанини і помилок.

Порада: Використовуйте std::string_view замість рядків C-style. Для рядків, які ви не плануєте в подальшому модифікувати, краще використовувати клас std::string_view замість std::string.

Функції, які модифікують представлення

Повернемося до нашої аналогії з вікном, тільки тепер розглянемо вікно з фіранками. Ми можемо закрити частину вікна лівою чи правою фіранкою, тим самим зменшивши те, що можна побачити крізь вікно. Зауважте, що ми не змінюємо об’єкти, що знаходяться зовні вікна, змінюється лише сектор спостереження з вікна.

Аналогічно і з класом std::string_view: в ньому містяться функції, що дозволяють нам управляти представленням рядка. Завдяки цьому ми можемо змінювати представлення рядка без зміни початкового рядка.

Для цього використовуються наступні функції:

   remove_prefix() — видаляє символи з лівої частини представлення;

   remove_suffix() — видаляє символи з правої частини представлення.

Наприклад:

Результат виконання програми:

Peach
each
ea

На відміну від справжніх фіранок, за допомогою яких ми можем закрити частину (або повністю) вікна, об’єкти класу std::string_view не можна “відкрити знову”. Змінивши один раз область видимості, ви вже не зможете повернутися до первинних значень (варто відзначити, що є рішення, які дозволяють вирішити дану проблему, але розглядати ми їх не будемо).

std::string_view і звичайні рядки

На відміну від рядків C-style, об’єкти класів std::string і std::string_view не використовують нульовий символ (нуль-термінатор) в якості мітки для позначення кінця рядка. Дані об’єкти знають, де закінчується рядок, тому що відстежують його довжину:

Результат виконання програми:

aeiou

Проблеми володінням і доступу

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

Результат виконання програми:

What's your name?
nascardriver
Hello nascardriver
Your name is �P@�P@

Коли ми оголосили змінну str і за допомогою std::cin присвоїли їй певне значення, то ця змінна створила всередині себе рядок, розмістивши його в динамічній області пам’яті. Після того, як змінна str вийшла за межі області видимості в кінці функції askForName(), внутрішній рядок припинив своє існування. При цьому об’єкт класу std::string_view не знає, що рядка більше не існує, і дозволяє нам до нього звернутися. Спроба доступу до такого рядка через його представлення в функції main() призводить до невизначеної поведінки, в результаті чого ми отримуємо кракозябри.

Така ж ситуація може відбутися і тоді, коли ми створюємо об’єкт std::string_view з об’єкта std::string, а потім модифікуємо початковий об’єкт std::string. Модифікація об’єкта std::string може призвести до створення в іншому місці нового внутрішнього рядка і подальшого знищення старого рядка. При цьому std::string_view продовжить “дивитися” в те місце, де був старий рядок, але його вже там не буде.

Попередження: Слідкуйте за тим, щоб початковий рядок, на який посилається об’єкт std::string_view, не виходив за межі області видимості і не змінювався до тих пір, поки використовується об’єкт std::string_view, який на нього посилається.

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

Об’єкти класу std::string_view НЕ конвертуються неявно в об’єкти класу std::string, але конвертуються при явній конвертації:

Результат виконання програми:

ball
ball

Конвертація std::string_view в рядок C-style

Деякі старі функції (такі як strlen()) працюють тільки з рядками C-style. Для того щоб перетворити об’єкт класу std::string_view в рядок C-style, ми спочатку повинні конвертувати його в об’єкт класу std::string:

Результат виконання програми:

ball has 4 letter(s)

Однак варто враховувати те, що створення об’єкта класу std::string щоразу, коли ми хочемо перетворити об’єкт std::string_view в рядок C-style, є витратною операцією, тому ми повинні по можливості уникати подібних ситуацій.

Функція data()

Доступ до початкового рядка об’єкту std::string_view можна отримати за допомогою функції data(), яка повертає рядок C-style. При цьому забезпечується швидкий доступ до даного рядка (як до рядка C-style). Але це слід використовувати тільки тоді, коли об’єкт std::string_view не був змінений (наприклад, за допомогою функцій remove_prefix() чи remove_suffix()) і пов’язаний з ним рядок має нуль-термінатор (так як це рядок C-style).

У наступному прикладі функція std::strlen() нічого не знає про std::string_view, тому ми передаємо їй функцію str.data():

Результат виконання програми:

balloon
7

Коли ми намагаємося звернутися до об’єкта класу std::string_view, який був змінений, функція data() може повернути зовсім не той результат, який ми очікували від неї отримати. У наступному прикладі показано, що відбувається, коли ми звертаємося до функції data() після зміни представлення рядка:

Результат виконання програми:

all has 6 letter(s)
str.data() is alloon
str is all

Очевидно, що даний результат — це не те, що ми планували побачити, і він є наслідком спроби функції data() отримати доступ до даних представлення std::string_view, яке було змінено. Інформація про довжину рядка втрачається при зверненні до нього через функцію data(). std::strlen і std::cout продовжують зчитувати символи з початкового рядка до тих пір, поки не зустрінуть нуль-термінатор, який знаходиться в кінці рядка baloon.

Попередження: Використовуйте std::string_view::data() тільки в тому випадку, коли представлення std::string_view не було змінено і рядок, який відображається, містить нуль-термінатор. Використання функції std::string_view::data() з рядком без нуль-термінатора може призвести до виникнення помилок.

Нюанси std::string_view

Будучи відносно недавнім нововведенням, клас std::string_view реалізований не так вже й ідеально, як хотілося б:

Немає ніяких причин для того, щоб рядки №5-6 не працювали, але тим не менше вони не працюють. Ймовірно, повна підтримка даного функціоналу буде реалізована в наступних версіях стандарту C++.

Оцінити статтю:

1 Зірка2 Зірки3 Зірки4 Зірки5 Зірок (4 оцінок, середня: 5,00 з 5)
Loading...

Залишити відповідь

Ваш E-mail не буде опублікований. Обов'язкові поля відмічені *