Краткий обзор string_view

Добавлено 7 февраля 2022 в 00:42

Возможности работы со строками в 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, это легкие объекты с богатым интерфейсом, которые позволяют писать выразительный код и платить только чуть больше, чем вы используете.

Теги

C++ / CppC++17std::string_viewSTL / Standard Template Library / Стандартная библиотека шаблоновПрограммирование

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

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