10.7 – Знакомство с std::string_view
В предыдущем уроке мы говорили о строках в стиле 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++.