На этом уроке мы рассмотрим класс std::string_view, который является нововведением стандарта С++17.
Проблемы строк
На уроке о строках C-style мы говорили об опасностях, которые возникают при их использовании. Конечно, строки C-style работают быстро, но при этом их использование не является таким уж простым и безопасным в сравнении с std::string.
Правда, стоит отметить, что и у std::string имеются свои недостатки, особенно когда речь заходит об использовании константных строк.
Рассмотрим следующий пример:
1 2 3 4 5 6 7 8 9 10 11 12 13 |
#include <iostream> #include <string> int main() { char text[]{ "hello" }; std::string str{ text }; std::string more{ str }; std::cout << text << ' ' << str << ' ' << more << '\n'; return 0; } |
Результат выполнения программы:
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
:
1 2 3 4 5 6 7 8 9 10 11 12 13 |
#include <iostream> #include <string_view> int main() { std::string_view text{ "hello" }; // представление для строки "hello", которое хранится в бинарном виде std::string_view str{ text }; // представление этой же строки - "hello" std::string_view more{ str }; // представление этой же строки - "hello" std::cout << text << ' ' << str << ' ' << more << '\n'; return 0; } |
В результате мы получим точно такой же вывод на экран, как и в предыдущем примере, но при этом у нас не будут созданы лишние копии строки hello
. Когда мы копируем объект класса std::string_view, то новый объект std::string_view будет «смотреть» на ту же самую строку, на которую «смотрел» исходный объект. Ко всему прочему, класс std::string_view не только быстр, но и обладает многими функциями, которые мы изучили при работе с классом std::string:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
#include <iostream> #include <string_view> int main() { std::string_view str{ "Trains are fast!" }; std::cout << str.length() << '\n'; // 16 std::cout << str.substr(0, str.find(' ')) << '\n'; // Trains std::cout << (str == "Trains are fast!") << '\n'; // 1 // Начиная с C++20 std::cout << str.starts_with("Boats") << '\n'; // 0 std::cout << str.ends_with("fast!") << '\n'; // 1 std::cout << str << '\n'; // Trains are fast! return 0; } |
Т.к. объект класса std::string_view не создает копии строки, то, изменив исходную строку, мы, тем самым, повлияем и на её представление в связанном с ней объектом std::string_view:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
#include <iostream> #include <string_view> int main() { char arr[]{ "Gold" }; std::string_view str{ arr }; std::cout << str << '\n'; // Gold // Изменяем 'd' на 'f' в arr arr[3] = 'f'; std::cout << str << '\n'; // Golf return 0; } |
Изменяя 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()
— удаляет символы из правой части представления.
Например:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 |
#include <iostream> #include <string_view> int main() { std::string_view str{ "Peach" }; std::cout << str << '\n'; // Игнорируем первый символ str.remove_prefix(1); std::cout << str << '\n'; // Игнорируем последние 2 символа str.remove_suffix(2); std::cout << str << '\n'; return 0; } |
Результат выполнения программы:
Peach
each
ea
В отличие от настоящих занавесок, с помощью которых мы закрыли часть окна, объекты класса std::string_view нельзя «открыть обратно». Изменив однажды область видимости, вы уже не сможете вернуться к первоначальным значениям (стоит отметить, что есть приемы, которые позволяют решить данную проблему, но вдаваться в них мы не будем).
std::string_view и обычные строки
В отличие от строк C-Style, объекты классов std::string и std::string_view не используют нулевой символ (нуль-терминатор) в качестве метки для обозначения конца строки. Данные объекты знают, где заканчивается строка, т.к. отслеживают её длину:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
#include <iostream> #include <iterator> // для функции std::size() #include <string_view> int main() { // Нет нуль-терминатора char vowels[]{ 'a', 'e', 'i', 'o', 'u' }; // Массив vowels не является нуль-терминированным. Мы должны передавать длину вручную. // Поскольку vowels является массивом, то мы можем использовать функцию std::size(), чтобы получить его длину std::string_view str{ vowels, std::size(vowels) }; std::cout << str << '\n'; // это безопасно, так как std::cout знает, как выводить std::string_view return 0; } |
Результат выполнения программы:
Проблемы владения и доступа
Поскольку std::string_view является всего лишь представлением строки, его время жизни не зависит от времени жизни строки, которую он представляет. Если отображаемая строка выйдет за пределы области видимости, то std::string_view больше не сможет её отображать и при попытке доступа к ней мы получим неопределенные результаты:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 |
#include <iostream> #include <string> #include <string_view> std::string_view askForName() { std::cout << "What's your name?\n"; // Используем std::string, поскольку std::cin будет изменять строку std::string str{}; std::cin >> str; // Мы переключаемся на std::string_view только в демонстрационных целях. // Если вы уже имеете std::string, то нет необходимости переключаться на std::string_view std::string_view view{ str }; std::cout << "Hello " << view << '\n'; return view; } // str уничтожается и, таким образом, уничтожается и строка, созданная str int main() { std::string_view view{ askForName() }; // view пытается обратиться к строке, которой уже не существует std::cout << "Your name is " << view << '\n'; // неопределенное поведение return 0; } |
Результат выполнения программы:
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, но конвертируются при явном преобразовании:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 |
#include <iostream> #include <string> #include <string_view> void print(std::string s) { std::cout << s << '\n'; } int main() { std::string_view sv{ "balloon" }; sv.remove_suffix(3); // print(sv); // ошибка компиляции: неявная конвертация запрещена std::string str{ sv }; // явное преобразование print(str); // ок print(static_cast<std::string>(sv)); // ок return 0; } |
Результат выполнения программы:
Конвертация std::string_view в строку C-style
Некоторые старые функции (такие как strlen()
) работают только со строками C-style. Для того чтобы преобразовать объект класса std::string_view в строку C-style, мы сначала должны конвертировать его в объект класса std::string:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 |
#include <cstring> #include <iostream> #include <string> #include <string_view> int main() { std::string_view sv{ "balloon" }; sv.remove_suffix(3); // Создание объекта std::string из объекта std::string_view std::string str{ sv }; // Получаем строку C-style с нуль-терминатором auto szNullTerminated{ str.c_str() }; // Передаем строку с нуль-терминатором в функцию, которую мы хотим использовать std::cout << str << " has " << std::strlen(szNullTerminated) << " letter(s)\n"; return 0; } |
Результат выполнения программы:
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()
:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
#include <cstring> // для функции std::strlen() #include <iostream> #include <string_view> int main() { std::string_view str{ "balloon" }; std::cout << str << '\n'; // Для простоты мы воспользуемся функцией std::strlen(). Вместо нее можно было бы использовать любую другую функцию, которая работает со строкой с нуль-терминатором в конце. // Здесь мы можем использовать функцию data(), так как мы не изменяли представление и строка имеет нуль-терминатор std::cout << std::strlen(str.data()) << '\n'; return 0; } |
Результат выполнения программы:
balloon
7
Когда мы пытаемся обратиться к объекту класса std::string_view, который был изменен, функция data() может вернуть совсем не тот результат, который мы ожидали от нее получить. В следующем примере показано, что происходит, когда мы обращаемся к функции data() после изменения представления строки:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 |
#include <cstring> #include <iostream> #include <string_view> int main() { std::string_view str{ "balloon" }; // Удаляем символ "b" str.remove_prefix(1); // Удаляем часть "oon" str.remove_suffix(3); // Помните, что предыдущие 2 команды не изменяют исходную строку, они работают лишь с её представлением std::cout << str << " has " << std::strlen(str.data()) << " letter(s)\n"; std::cout << "str.data() is " << str.data() << '\n'; std::cout << "str is " << str << '\n'; return 0; } |
Результат выполнения программы:
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 реализован не так уж и идеально, как хотелось бы:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
std::string s{ "hello" }; std::string_view v{ "world" }; // Не работает std::cout << (s + v) << '\n'; std::cout << (v + s) << '\n'; // Потенциально небезопасно или не то, что мы хотим получить, // поскольку мы пытаемся использовать объект std::string_view в качестве строки C-style std::cout << (s + v.data()) << '\n'; std::cout << (v.data() + s) << '\n'; // Приемлемо, т.к. нам нужно создать новый объект std::string, но некрасиво и нерационально std::cout << (s + std::string{ v }) << '\n'; std::cout << (std::string{ v } + s) << '\n'; std::cout << (s + static_cast<std::string>(v)) << '\n'; std::cout << (static_cast<std::string>(v) + s) << '\n'; |
Нет никаких причин для неработоспособности строк №5-6, но тем не менее они не работают. Вероятно, полная поддержка данного функционала будет реализована в следующих версиях стандарта C++.
Шел 2021 год, а в С++ строках std::string все так же нет поддержки юникодных символов (попробуйте поменять регистр не латинского текста вылезет кучу проблем). Нет хотябы базового функционала который есть в строках на том же Python.
Вместо того чтобы доводить до ума один из важнейших кирпичиков языка, мы получили "более лучший" std::string_view который делает все тоже что и нормальная строка, но при этом не хранит ее значение, то есть мы в коде обязаны иметь запасную строку чтобы string_view работал.
При этом согласно все тому же С++ 17, std::string больше не копируется и автоматически используется move, то есть нужды использовать некоторый "быстрый" придаток к строке еще меньше.
И все это сделано в угоду некоторой "производительности". Но все и так понимают что там где важна производительность никто не будет тащить С++, все пишется на чистом быстром C где все отточено до скорости "портативного ассемблера".
Для юникодных строк, вроде, есть wstring, u16string и u32string.
А в чем разница между использованием const std::string & и std::string_view?
Представьте, что вы передаете в функцию в качестве параметра string_view литерал в двойных кавычках (char*, временный объект). В случае string произойдет копирование во временный объект типа string за линейное время от длины строки, а в случае со string_view это почти бесплатно – string_view внутри себя хранит только сам указатель, а не массив типа char на куче, например. Но тут тоже, как я предполагаю, придется пройтись по всей строке и посчитать ее длину. Все-таки это сильно эффективнее по памяти, если строка очень длинная. Один указатель и число против огромного массива.
Понятно, спасибо
Очень полезно что автор знакомит с разными стандартами, и плюсами\минусами новых стандартов.
"Нет никаких причин на то, почему бы строки №5-6 были бы неработоспособными, но, тем не менее, они не работают. Вероятно, полная поддержка данного функционала будет реализована в следующих версиях стандарта C++."
Прочитать, чтобы понять — что std::string_view пока что фуфло. И лучше использовать обычный string, поскольку ни одного преимущества выявить не удалось.
imho, все на производительность заточено. по сути std::string покрывает все потребности, но это неэффективно в некоторых случаях … очень _не_ эффективно. с++ с этим не может мириться (с такими накладными расходами по памяти и процессору) и вот … частное решение std::string_view, которое частично (т.е. если задача позволяет) эту проблему хоть как-то сглаживает.