Проблема: функции, берущие слишком много в собственность

Функция, обрабатывающая буфер, часто выглядит так:

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 фиксирован

Что дальше