В API библиотеки нужно принимать любой непрерывный участок памяти без копирования и без привязки к конкретному контейнеру. Какой стандартный тип C++20 для этого выбрать и почему?
Выберите std::span: это невладеющее представление непрерывного участка памяти, содержащее указатель на первый элемент и количество элементов. Оно позволяет одной функции принимать массивы, std::array, std::vector и другие contiguous-диапазоны без копирования и без шаблонного параметра контейнера.
std::span не продлевает время жизни данных, поэтому вызывающая сторона обязана гарантировать, что исходный буфер живёт дольше всех операций через это представление.
До C++20 для передачи участка массива обычно использовали пару «указатель плюс размер», пару итераторов или отдельные шаблонные перегрузки для разных контейнеров. Такие интерфейсы хуже выражали намерение: из типа параметров не всегда было понятно, что требуется именно непрерывный диапазон и что функция не владеет данными.
std::span появился как стандартное лёгкое представление непрерывной памяти. Он решает проблему унификации интерфейса, сохраняя нулевую стоимость владения: объект span обычно состоит только из указателя и размера.
Если функция принимает контейнер по значению, вызов может создать ненужную копию. Если она принимает конкретный тип контейнера, интерфейс становится излишне связанным с реализацией вызывающего кода.
Пара «указатель плюс размер» устраняет копирование, но допускает несогласованные значения: указатель может быть нулевым, размер — неверным, а требование непрерывности памяти вообще не выражается типом. Передача только указателей или итераторов также не даёт функции размера участка.
Неверное использование span приводит к неопределённому поведению: например, сохранённое представление может указывать на уже уничтоженный контейнер или на память после перевыделения std::vector.
std::span<T, Extent> содержит представление последовательности элементов типа T. При динамическом размере используется std::dynamic_extent, а фиксированный размер может быть частью типа, например std::span<const int, 4>.
Тип элемента определяет доступность записи. std::span<int> позволяет изменять элементы, а std::span<const int> — только читать их. Это отделяет вопрос владения от вопроса изменяемости: span никогда не владеет памятью, но может предоставлять изменяемый или константный доступ.
Вызов не копирует элементы: функция получает только представление. Интерфейс принимает именно непрерывные диапазоны; например, std::list напрямую передать нельзя, потому что его элементы не лежат в одном непрерывном участке памяти.
Главное ограничение — время жизни. span безопасен только пока существует исходный буфер и пока операция, изменяющая его расположение, не инвалидировала указатель. Поэтому span хорошо подходит для параметра функции, но его сохранение в объекте требует явно документированной гарантии времени жизни.
Фиксированная и динамическая протяжённость имеют разные компромиссы. Фиксированная протяжённость позволяет закодировать размер в типе и проверять его при создании представления, но уменьшает универсальность интерфейса. std::dynamic_extent удобнее для участков произвольного размера и обычно является правильным выбором для общего API.
Компонент сериализации должен вычислять контрольную сумму для данных из std::vector, встроенного массива и буфера, полученного от сетевой библиотеки. Вариант с std::vector потребовал бы копирования или преобразований, поэтому он не подходит для горячего пути.
Параметр «указатель плюс размер» был бы совместимым решением, но не показывал бы в типе требование непрерывной памяти и допускал бы ошибочную передачу размера. Шаблонный параметр диапазона дал бы гибкость, однако усложнил бы интерфейс и мог бы породить много инстанцирований.
Выбран std::span<const std::byte> или соответствующий std::span<const T> на границе API. Функция читает данные без копирования, принимает разные источники памяти, а контракт явно сообщает, что владение буфером не передаётся. При этом span не сохраняют после завершения вызова, что устраняет риск висячего представления.
std::span?Временный контейнер уничтожается в конце полного выражения. Если span используется только внутри вызова функции, это обычно безопасно: исходный объект живёт до завершения вызова. Если же представление сохраняется, возвращается или передаётся в асинхронную задачу, оно станет висячим после уничтожения временного контейнера.
Нельзя считать сам факт успешного создания span гарантией длительного времени жизни данных. Ограничения конструкторов могут предотвращать некоторые опасные преобразования, особенно для неконстантного span, но они не делают сохранённое представление владеющим и не заменяют проектирование времени жизни.
std::span<T, N>?Фиксированный размер является частью типа и сообщает, что представление должно содержать ровно N элементов. Это полезно для интерфейсов, принимающих, например, ровно четыре значения; несоответствие размера обнаруживается при создании или преобразовании span.
У фиксированного span размер может не храниться отдельно, поэтому представление потенциально компактнее. Цена — меньшая универсальность: span с другим размером нельзя передать без подходящего преобразования. Для обычного буфера произвольного размера предпочтителен std::span<T> с динамической протяжённостью.
std::span для любого диапазона?Нет. std::span предназначен только для непрерывной памяти и индексируемого доступа к элементам. Контейнеры с раздельным размещением узлов, такие как std::list или std::map, не удовлетворяют этому требованию.
Для общего, возможно неконтiguous диапазона следует рассматривать шаблонный параметр диапазона, пару итераторов или C++20 ranges-интерфейс. span выбирают именно тогда, когда алгоритму нужна непрерывность и желательно получить простой невладеющий вид без копирования.