Проблема: функции, берущие слишком много в собственность
Функция, обрабатывающая буфер, часто выглядит так:
1void process(const std::vector<float>& data);
Это работает — но излишне ограничено. Вызывающий должен иметь std::vector.
Массив, внутреннее хранилище кольцевого буфера, DMA-буфер на стеке — ничего из
этого нельзя передать без копирования или reinterpret.
Или сырой вариант с указателем:
1void process(const float* data, size_t len);
Это работает с любым непрерывным хранилищем — но теперь снова сырые указатели
без информации о границах в типе, и ничто не мешает вызывающему передать неправильный len.
std::span<const float> — правильный инструмент: невладеющее представление любой
последовательности float-ов, с размером встроенным в тип, нулевыми накладными
расходами и проверкой границ в отладочных сборках.
std::span — представление непрерывных данных
std::span<T> — пара (указатель, длина). Не владеет данными — не выделяет и не копирует.
Это окно в существующую память.
1#include <span>
2
3void process(std::span<const float> data) {
4 for (float v : data) { // range-for работает
5 handle(v);
6 }
7 float first = data[0]; // operator[] с проверкой границ в отладке
8 size_t n = data.size();
9 const float* ptr = data.data(); // доступ к сырому указателю при необходимости
10}
Функция теперь принимает любой непрерывный источник:
1std::vector<float> vec = {1, 2, 3};
2process(vec); // неявное преобразование
3
4float arr[] = {1, 2, 3};
5process(arr); // работает
6
7std::array<float, 3> a = {1, 2, 3};
8process(a); // работает
9
10float dma_buf[DMA_SIZE];
11process(std::span(dma_buf, DMA_SIZE)); // DMA-буфер — без копирования
Одна сигнатура функции — для любого непрерывного хранилища.
Статический vs динамический размер
1std::span<float> // динамический размер — хранится в памяти во время выполнения
2std::span<float, 8> // статический размер — известен на этапе компиляции, sizeof = один указатель
Используйте статический размер, когда он всегда фиксирован — убирает хранимую длину и делает span дешевле при передаче.
1void processAdc(std::span<const uint16_t, 4> channels) {
2 // Всегда ровно 4 канала — проверяется на этапе компиляции
3}
4
5std::array<uint16_t, 4> buf = {0};
6processAdc(buf); // OK — размеры совпадают
Подspan’ы
1std::span<float> full = buf;
2auto first_half = full.subspan(0, full.size() / 2);
3auto last_half = full.subspan(full.size() / 2);
4auto first_three = full.first(3);
5auto last_three = full.last(3);
Полезно для разделения двойного DMA-буфера без арифметики указателей:
1void processHalf(std::span<const uint16_t> half);
2
3// В DMA callback:
4std::span<const uint16_t> buf(dmaBuf, DMA_BUF_SIZE);
5processHalf(buf.first(DMA_BUF_SIZE / 2)); // callback HT
6processHalf(buf.last (DMA_BUF_SIZE / 2)); // callback TC
std::string_view — представление символьных данных
std::string_view — это std::span<const char> со строковыми операциями:
find, substr, starts_with, ends_with и т.д.
1#include <string_view>
2
3void log(std::string_view msg) {
4 uart_send(msg.data(), msg.size());
5}
6
7log("boot complete"); // строковый литерал — без выделения
8log(std::string("hello")); // std::string — без копирования
9log(buf.data()); // массив char — без копирования
Замена const std::string&
Классическое правило: заменяйте параметры const std::string& на std::string_view:
1// До: вынуждает вызывающего иметь std::string
2void setName(const std::string& name);
3
4// После: принимает всё
5void setName(std::string_view name);
Единственное исключение: если функция сохраняет строку (в член, в map и т.д.),
ей всё равно придётся копировать в std::string — тип параметра в этом случае
не особо важен, хотя string_view всё равно более общий.
Безопасная подстрока без выделения
1std::string_view parse(std::string_view input) {
2 auto pos = input.find(':');
3 if (pos == std::string_view::npos) return input;
4 return input.substr(pos + 1); // подстрока без копирования
5}
6
7std::string_view value = parse("key:value");
8// value указывает в исходную строку — без выделения
Ловушка времени жизни
И span, и string_view не владеют данными. Они не должны переживать данные,
на которые указывают. Это основная точка риска.
1std::string_view danger() {
2 std::string s = "hello";
3 return s; // UB — s уничтожается, представление висит в воздухе
4}
5
6std::span<int> also_danger() {
7 int arr[] = {1, 2, 3};
8 return arr; // UB — arr уничтожается при возврате
9}
Правило: span и string_view должны быть параметрами функций и локальными
переменными, а не возвращаемыми типами или членами класса (если только время жизни
не управляется явно и не задокументировано).
1// OK — span живёт в области видимости буфера
2void process() {
3 uint8_t buf[64];
4 fill(buf);
5 auto view = std::span(buf); // та же область видимости — безопасно
6 analyze(view);
7}
8
9// НЕПРАВИЛЬНО — представление-член на временный
10class Parser {
11 std::string_view view_; // опасно — на что указывает?
12};
Применение во встраиваемых системах
Разбор фреймов протокола без копирования:
1struct Frame {
2 uint8_t cmd;
3 std::span<const uint8_t> payload;
4};
5
6Frame parseFrame(std::span<const uint8_t> raw) {
7 return { raw[0], raw.subspan(1) };
8 // payload указывает в raw — нулевое копирование
9}
Запись в предварительно выделенный выходной буфер:
1size_t serialize(const SensorData& d, std::span<uint8_t> out) {
2 if (out.size() < sizeof(SensorData)) return 0;
3 std::memcpy(out.data(), &d, sizeof(d));
4 return sizeof(d);
5}
Единообразная обработка строк во flash и RAM:
1void sendResponse(std::string_view msg) {
2 for (char c : msg) uart_putc(c);
3}
4
5sendResponse("OK\r\n"); // литерал во flash
6sendResponse(buildError(code)); // построен в RAM
Когда НЕ использовать
| Ситуация | Использовать вместо |
|---|---|
| Сохранение строки за пределами вызова | std::string |
| Непоследовательные данные (list, deque) | Пара итераторов / диапазон |
| Владеющий буфер | std::vector или std::array |
| Передача владения | unique_ptr или семантика перемещения |
| Требуется null-терминированная строка для C API | const char* (из .data()) |
Итоги
std::span<const T>заменяетconst std::vector<T>&и(const T*, size_t)для непрерывных данных только для чтенияstd::string_viewзаменяетconst std::string&для любой строки только для чтения- Оба имеют нулевые накладные расходы — один указатель и один размер, передаются в регистрах
- Оба не владеют данными — никогда не храните их как члены без управления временем жизни
- Статический размер
span<T, N>полностью устраняет поле размера, когда N фиксирован
Что дальше
- Руководство по выбору контейнера — когда лучше vector, array, deque или list
- Семантика перемещения и передача без копирования — передача владения без span
- Lock-free очереди — передача span’ов между производителем и потребителем