9.7 – Знакомство с std::string_view

Добавлено 6 июня 2021 в 18:59
Глава 9 – Массивы, строки, указатели и ссылки  (содержание)

В предыдущем уроке мы говорили о строках в стиле C и об опасностях их использования. Строки в стиле C бывают быстрыми, но они не так просты в использовании и не так безопасны, как std::string.

Но std::string (который мы рассмотрели в уроке «4.12 – Знакомство с std::string») имеет свои недостатки, особенно когда речь идет о константных строках.

Рассмотрим следующий пример:

#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[]. Следующие два объекта std::string создают по одной копии строки каждый. Поскольку std::string спроектирован таким образом, чтобы его можно было изменять, каждый объект std::string должен содержать свою собственную копию строки, чтобы данный объект std::string мог быть изменен, не затрагивая любой другой объект std::string.

Это верно и для константных std::string, даже если их нельзя изменить.

Знакомство с std::string_view

Представьте себе окно в вашем доме, открывающую вид на машину, стоящую на улице. Вы можете посмотреть в окно и увидеть машину, но ее нельзя потрогать или передвинуть. Ваше окно просто обеспечивает вид на машину, которая является совершенно отдельным объектом.

C++17 представляет другой способ использования строк, std::string_view, который находится в заголовке <string_view>.

В отличие от std::string, который хранит свою собственную копию строки, std::string_view обеспечивает представление строки, определенной в другом месте.

Мы можем переписать приведенный выше код для использования std::string_view, заменив каждый std::string на std::string_view.

#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". Строка "hello" хранится в двоичном файле и дополнительно не размещается в памяти во время выполнения. text – это только представление строки "hello", поэтому создавать копию не нужно. Когда мы копируем std::string_view, новый std::string_view наблюдает ту же строку, что и std::string_view, из которого эта строка была скопирована. Это означает, что ни str, ни more не создают никаких копий. Это представления существующей строки "hello".

std::string_view не только быстр, но имеет множество функций, которые мы знаем из std::string.

#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.

#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, похоже, тоже меняется. Это потому, что arr и str имеют общую строку. Когда вы используете std::string_view, лучше избегать изменений базовой строки до конца жизни std::string_view, чтобы избежать путаницы и ошибок.

Лучшая практика


Используйте std::string_view вместо строк в стиле C.

Для строк, доступных только для чтения, предпочитайте использование std::string_view вместо std::string, если у вас еще нет std::string.

Функции модификации представления

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

Точно так же std::string_view содержит функции, которые позволяют нам управлять представлением строки. Это позволяет нам изменять представление без изменения представленной строки.

Функции для этого – remove_prefix, которая удаляет символы с левой стороны представления, и remove_suffix, которая удаляет символы с правой стороны представления.

#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 и std::string, std::string_view не использует нулевые терминаторы для обозначения конца строки. Вернее, он знает, где заканчивается строка, потому что отслеживает ее длину.

#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 знает, как печатать std::string_views.
  std::cout << str << '\n'; 
 
  return 0;
}

Эта программа печатает:

aeiou

Вопросы собственности

Будучи всего лишь представлением, время жизни std::string_view не зависит от времени жизни представляемой строки. Если представляемая строка выходит за пределы области видимости, std::string_view нечего представлять, и доступ к нему вызывает неопределенное поведение. Строка, которую представляет std::string_view, должна быть создана где-то еще. Это может быть строковый литерал, который живет, пока работает программа, или она может быть создана с помощью std::string, и в этом случае строка живет до тех пор, пока std::string не решит ее уничтожить, или пока не будет уничтожен сам std::string. std::string_view не может создавать какие-либо строки самостоятельно, потому что это просто представление.

#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() };
 
  // представление наблюдает за строкой, которая уже уничтожена.
  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, внутренняя строка уничтожается вместе с str. std::string_view не знает, что строка больше не существует, и позволяет нам получить к ней доступ. Доступ к освобожденной строке через 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, но может быть преобразован явно:

#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;
}

Этот код напечатает:

ball
ball

Преобразование std::string_view в строку в стиле C

Некоторые старые функции (например, старая функция strlen) всё еще ожидают строк в стиле C. Чтобы преобразовать std::string_view в строку в стиле C, мы можем сделать это, сначала выполнив преобразование в std::string:

#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
  const char* 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, – дорогостоящая операция, поэтому по возможности этого следует избегать.

Открытие (как бы) окна через функцию data()

Доступ к строке, просматриваемой std::string_view, можно получить с помощью функции data(), которая возвращает строку в стиле C. Это обеспечивает быстрый доступ к просматриваемой строке (как к строке в стиле C). Но это также следует использовать только в том случае, если представление std::string_view не было изменено (например, с помощью remove_prefix или remove_suffix), а просматриваемая строка оканчивается нулем.

В следующем примере std::strlen не знает, что такое std::string_view, поэтому нам нужно передать его с помощью str.data():

#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() после изменения представления:

#include <cstring>
#include <iostream>
#include <string_view>
 
int main()
{
  std::string_view str{ "balloon" };
 
  // удаляем "b"
  str.remove_prefix(1);
  // удаляем "oon"
  str.remove_suffix(3);
  // Помните, что приведенный выше код не изменяет строку,
  // а изменяет только область, за которой наблюдает str.
 
  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 продолжают читать символы из базовой строки, пока не найдут нулевой терминатор, который находится в конце "balloon".

Предупреждение


Используйте std::string_view::data() только в том случае, если представление std::string_view не было изменено и просматриваемая строка оканчивается нулем. Использование std::string_view::data() для строки, не завершающейся нулем, может привести к неопределенному поведению.

Незавершенная реализация

Поскольку std::string_view появился относительно недавно, он реализован не так хорошо, как мог бы.

std::string s{ "hello" };
std::string_view v{ "world" };
 
// Не работает
std::cout << (s + v) << '\n';
std::cout << (v + s) << '\n';
 
// Потенциально небезопасно или не то, что нам нужно, потому что
// мы работаем с std::string_view как со строкой в стиле C.
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++.

Теги

C++ / CppLearnCppstd::stringstd::string_viewДля начинающихОбучениеПрограммированиеСтрокаСтрока в стиле C

На сайте работает сервис комментирования DISQUS, который позволяет вам оставлять комментарии на множестве сайтов, имея лишь один аккаунт на Disqus.com.

В случае комментирования в качестве гостя (без регистрации на disqus.com) для публикации комментария требуется время на премодерацию.