std::span – это объект, который ссылается на непрерывную последовательность объектов (относительно расположения в памяти). Иногда std::span называют видом, и при этом он никогда не является владельцем данных. Непрерывная последовательность объектов может быть массивом языка С, указателем с размером, std::array, std::vector или std::string. У std::span может быть статическая (static extent) или динамическая длина (изменяемая, dynamic extent). По умолчанию у std::span динамическая длина.
Определение std::span
Статическая и динамическая длина
Когда у std::span статическая длина, то его размер известен во время компиляции и является частью типа: std::span<T, size>. Соответственно, реализации нужен только указатель на первый элемент из непрерывной последовательности объектов.
Здесь число 5 является частью типа. Если попытаться инициализировать span массивом с другим размером, будет ошибка компиляции.
Реализация std::span с динамической длиной состоит из указателя на первый элемент и размера непрерывной последовательности объектов. Размер не является частью типа: std::span<T>.
Интересной особенностью спана является то, что при статической длине он занимает меньше памяти, так как нет необходимости хранить внутри размер последовательности:
Вывод:
Следующий пример показывает отношения между спанами разного типа длины.
Вывод:
У dynamicSpan динамическая длина, в то время как у staticSpan статическая длина. Оба std::span’а возвращают свой размер в функции printMe. Можно присвоить std::span-у с динамической длиной std::span со статической длиной, но не наоборот. Если раскомментировать строку staticSpan = dynamicSpan; будет ошибка компиляции.
Одной из важных причин существования std::span<T> является то, что обычный массив С приводится к указателю при передаче в функцию, тем самым его размер теряется. Это является одной из типичных причин возникновения ошибок в С/C++.
Автоматический вывод размера непрерывной последовательности объектов
В отличие от массива в С, std::span<T> автоматически выводит размер непрерывной последовательности объектов.
Вывод:
Массив из С, std::vector и std::array содержат значения типа int. Поэтому std::span тоже содержит значения типа int. Но в этом примере есть один интересный момент – для каждого контейнера std::span может вывести его размер.
Методы создания std::span
Вы можете создать std::span из указателя и размера последовательности объектов, из пары итераторов, из итератора и размера, из ссылки на непрерывный контейнер объектов, или из другого спана. Дефолтный конструктор создает пустой спан.
Вывод:
Как вы и могли ожидать все спаны в этом примере проходят тест на идентичность. Здесь также показана техника сравнения спанов: мы не можем использовать просто == так как спан инкапсулирует указатель и размер, поэтому используется алгоритм std::equal. Этот алгоритм в свою очередь проверяет кол-во элементов равное размеру первого входного диапазона, и если второй диапазон длиннее, результат будет неправильным, поэтому к проверке добавляется тест на равенство размера.
std::span – это не std::string_view и не ranges::view. Вы можете вспомнить, что иногда std::span называют видом. Но не путайте std::span с видом из библиотеки диапазонов и std::string_view. Вид из библиотеки диапазонов – это что-то, что вы можете применить к диапазону и выполнить некоторую операцию. Вид не владеет данными, и его время копирования, перемещения и присваивания константно. При этом std::span и std::string_view – это не владеющие виды, и они могут работать со строками. Основная разница между ними в том, что std::span может изменять объекты, на которые он ссылается.
Изменение объектов, к которым происходит обращение через ссылку
Вы можете изменить весь диапазон (span) или только его часть. Когда вы изменяете диапазон, то вы изменяете объекты, на которые он ссылается. Следующая программа демонстрирует то, как можно изменить объекты из std::vector.
Вывод:
Здесь span1 ссылается на std::vector vec. В отличие от этого, span2 ссылается на элементы vec, за исключением первого и последнего. Соответственно, замена каждого элемента на его возведение в квадрат касается только этих элементов.
Есть целый ряд удобных функций, с помощью которых можно обращаться к элементам std::span.
Обращение к элементам std::span
Следующая таблица приводит список функций для доступа к элементам std::span.
Программа демонстрирует использование функции subspan:
Вывод:
Сначала вектор заполняется числами от 0 до 19 при помощи алгоритма std::iota. Этот вектор далее применяется для инициализации std::span. Наконец, цикл for использует функцию subspan для создания всех подпоследовательностей, начинающихся с элемента first и содержащих count элементов, пока не будет перебран весь mySpan.
Постоянный диапазон изменяемых элементов
Обычно std::vector и std::string моделируют изменяемый диапазон изменяемых элементов. Когда вы их объявляете как const, то получаете неизменяемый диапазон неизменяемых элементов. Таким образом, вы не можете создать неизменяемый диапазон изменяемых элементов. И здесь нам на помощь приходит std::span. std::span моделирует неизменяемый диапазон изменяемых элементов: std::span<T>. Следующая таблица сводит вместе все возможные вариации использования std::span.
Программа показывает каждую комбинацию:
Вывод:
Вектор dynamicVec – это модифицируемый диапазон модифицируемых элементов. Но это не относится к constVec. В случае constVec нельзя изменить ни элементы, ни размер. constSpan ведет себя аналогично. dynamicSpan соответствует неизменяемому диапазону с изменяемыми элементами.
std::span – это объект, который ссылается на непрерывную последовательность объектов. std::span, иногда называемый видом, никогда не является владельцем и поэтому не выделяет память. Непрерывная последовательность объектов может быть массивом С, указателем вместе с размером, std::array, std::vector или std::string. В отличие от массива в С, std::span автоматически выводит размер последовательности объектов, на которую он ссылается. Когда std::span изменяет свои элементы, то объекты, на которые он ссылается, также изменяются.
C++26
В стандарте C++26 для спана добавлен метод at(), который обеспечивает безопасный доступ к элементам, также как и std::vector::at(). При указании индекса вне границ диапазона бросается исключение std::out_of_range.
Для сравнения, std::string_view может быть создан из строковых литералов, в то время как std::span<const T> не может быть создан из {1, 2, 3}, что ограничивает использование спана в качестве параметров функций. В стандарте C++26 добавлена возможность создавать спан из std::initializer_list.
Важные замечания - Как и std::string_view, std::span не увеличивает время жизни временных данных. При неправильном использовании это может привести к появлению висячих ссылок:
std::span<const int> sp = {1, 2, 3};
Это означает, что std::span над std::initializer_list лучше всего использовать для параметров функций, где span нужен только в пределах области видимости функции.
std::span в качестве возвращаемых значений
Несмотря на то, что спаны отлично подходят в качестве входных параметров, они могут проявить себя и в качестве возвращаемого типа. Это может быть нечто среднее между ссылками и копиями, с дополнительной гибкостью для обработки отсутствующих значений. Рассмотрим следующий пример, где есть менеджер конфигурации, и мы возвращаем Config с помощью функции-члена getConfig:
Используя span, мы можем четко указать тип-вид. Кроме того, span поддерживает разные контейнеры, поэтому даже если мы изменим внутреннее представление элемента конфигурации, мы все равно сможем вернуть объект span (при условии, что он все еще непрерывный). Недостаток этого подхода в том, что время жизни объекта dbConfig ограничено временем жизни объекта manager, и при неосторожном использовании можно получить "провисший" спан.
Особенность std::span основанного над std::vector
Как известно std::vector в некоторых ситуациях реаллоцирует память, и по этой причине делает недействительными все существующие итераторы, ссылки и указатели, а следовательно и спаны тоже.
Вывод:
Здесь видно что после каждой операции над вектором, указатель на начало вектора имеет разные значения адреса в памяти, поэтому спан в этой ситуации теряет актуальность.