Краткий обзор string_view
Возможности работы со строками в C++ мало менялись со времен C++98, пока в C++17 не произошло серьезное развитие: std::string_view
.
Давайте посмотрим, что такое string_view
, и что он может привнести в ваш код, сделав его более выразительным и заставив его работать быстрее.
std::string_view
Как следует из названия, std::string_view
– это представление строки. Но давайте определимся, что такое представление, и что такое строка.
Представление...
Представление – это легкий объект, который можно создавать, копировать, перемещать и присваивать за константное время, и который ссылается на другой объект.
Мы можем провести параллель с представлениями диапазонов в C++20, которые моделируют концепт std::ranges::view
. Этот концепт требовал, чтобы представления можно было копировать, перемещать и присваивать за константное время, и чтобы представления обычно ссылались на другие диапазоны.
В C++17 не было концептов и диапазонов, но std::string_view
уже имел семантику представления. Обратите внимание, что std::string_view
предназначен только для чтения. Он не может изменять символы в строке, на которую ссылается.
Также обратите внимание, что вам не нужен C++17, чтобы использовать string_view
. Есть несколько реализаций, совместимых с C++11, например, Abseil.
… на строку
Представление ссылается на что-то, и здесь std::string_view
ссылается на строку. Обозначение «string» включает в себя три вещи:
std::string
;char*
с завершающим нулем;char*
и размер.
Это три типа входных данных, которые вы можете передать для построения строки. Первый определен в классе std::string
как оператор неявного преобразования, а два последних соответствуют конструкторам std::string_view
.
Таким образом, std::string_view
– это легковесный объект, который ссылается на строку C или C++. Теперь давайте посмотрим, чем это может быть полезно для вашего кода.
Богатый API за дешево
Вернемся к истории строк в C++.
Корни std::string
До C++ в C не было строкового класса. C вынуждает нас работать с указателями char*
, что имеет два недостатка:
- нет четкого владения массивом символов;
- API для работы с ними очень ограничен.
Как упоминает Скотт Мейерс в конце книги «Более эффективный C++», при создании языка C++ «как председателю рабочей группы по стандартной библиотеке C++ Майку Вилоту сказали: «Если не будет стандартного строкового типа, то будет кровь на улицах!»». И в С++ появился класс std::string
.
std::string
решает две вышеупомянутые проблемы char*
, так как std::string
владеет своими символами и имеет дело со связанной памятью, а также имеет очень богатый интерфейс, который может делать очень много вещей (он настолько велик, что Герб Саттер описывает его «монолитный» аспект в последних 4 главах книги «Exceptional C++»).
Цена владения
Владение и управление памятью массива символов – большое преимущество, без которого мы не можем представить, как бы мы жили сегодня. Но за это приходится платить: каждый раз, когда мы создаем строку, она должна выделять память в куче (при условии, что в ней слишком много символов, чтобы не поместиться в оптимизацию маленькой строки). И каждый раз, когда мы ее уничтожаем, ей приходится возвращать эту память в кучу.
Эти операции задействуют ОС и требуют времени. Однако в большинстве случаев они остаются незамеченными, потому что большая часть кода статистически не критична для производительности. Но в коде, который чувствителен к производительности (и только ваш профилировщик может сказать вам, что это за код), многократное создание и удаление std::string
может быть неприемлемым для производительности.
Для иллюстрации рассмотрим следующий пример. Представьте, что мы создаем API логгирования, который использует std::string
, потому что это наиболее естественный способ сделать реализацию выразительной за счет использования богатого API. Нам даже в голову не придет использовать char*
:
void log(std::string const& information);
Мы обязательно берем строку по константной ссылке, чтобы избежать копирований, которые заняли бы время.
Теперь мы вызываем наш API:
log("The system is currently computing the results...");
Обратите внимание, что мы передаем const char*
, а не std::string
. Но log
ожидает std::string
. Этот код компилируется, потому что const char*
неявно преобразуется в std::string
… но, несмотря на const&
, этот код создает и разрушает std::string
!
Действительно, std::string
– это временный объект, созданный для функции log
, и он уничтожается в конце инструкции, вызывающей функцию.
char*
может исходить из строковых литералов, как в приведенном выше примере, а также из устаревшего кода, который не использует std::string
.
Если это происходит в чувствительной к производительности части кодовой базы, это может быть слишком большим ударом по производительности.
Что тогда делать? До string_view
нам пришлось вернуться к char*
и отказаться от выразительности реализации log
:
void log(const char* information); // плачущий смайлик
Использование std::string_view
С помощью std::string_view
мы можем получить лучшее из обоих миров:
void log(std::string_view information);
Это создает не std::string
, а просто легкое представление на const char*
. Так что больше никакого влияния на производительность. Но мы по-прежнему получаем все прелести API std::string
для написания выразительного кода в реализации log
.
Обратите внимание, что мы передаем string_view
путем копирования, так как он имеет семантику ссылки.
Подводный камень: управление памятью
Поскольку std::string_view
ссылается на строку и не владеет ею, мы должны убедиться, что указанная строка переживет string_view
. В приведенном выше коде всё выглядело нормально, но если мы не будем осторожны, у нас могут возникнуть проблемы с памятью.
Например, рассмотрим этот код, упрощенный для наглядности:
std::string_view getName()
{
auto const name = std::string{"Arthur"};
return name;
}
Это приводит к неопределенному поведению: функция возвращает std::string_view
, указывающий на объект std::string
, который был уничтожен в конце функции.
Эта проблема не нова и относится не только к std::string_view
. Она актуальна для указателей, для ссылок и вообще для любого объекта, который ссылается на другой:
int& getValue()
{
int const value = 42;
return value;
} // переменная value уничтожена!
Еще больше представлений в C++
Как упоминалось ранее, C++20 вводит формальную концепцию представления для диапазонов и добавляет в стандарт намного больше представлений. К ним относятся transform
, filter
и другие адаптеры диапазонов, которые являются одними из преимуществ библиотеки диапазонов.
Как и string_view
, это легкие объекты с богатым интерфейсом, которые позволяют писать выразительный код и платить только чуть больше, чем вы используете.