跳转至

嵌入式C++教程:std::span——轻量、非拥有的数组视图

std::span 想象成 C++ 里的「透明的传送带」:它不拥有上面的货物(内存),只是平静又高效地告诉你"这里有多少个元素、从哪里开始"。在嵌入式里,我们经常需要把一段内存传给函数——既不想拷贝,也不想丢失类型信息或边界信息,std::span 就是为这种场景生的。

或者说,直到C++20,一个标准的视图容器才出现。

  • std::span<T>非拥有(non-owning)的视图:不负责内存释放。
  • 它通常是一个指针 + 长度(非常轻量,拷贝成本低)。
  • 函数参数用 std::span<const T> 可以优雅地接受 T[]std::arraystd::vector、裸指针+长度 等多种来源。
  • 关键注意:不要让 span 的生存期超过底层数据的生存期 —— 悬垂指针依旧会把你咬一口。

引子:为什么不直接用指针或 vector?

在嵌入式代码里,我们常看到这样的函数签名:

void process_buffer(uint8_t* buf, size_t n);

这招确实灵活,但缺点:读者得同时记住 buf 的类型、长度单位是"元素数"还是"字节数"、函数是否要修改数据……出错的地方太多。 std::span 把这些语义显式化:类型和值(length)都在同一个对象里,阅读性和安全性都提升了。


基本用法

#include <span>
#include <vector>
#include <array>
#include <iostream>

void print_bytes(std::span<const uint8_t> s) {
    for (auto b : s) std::cout << std::hex << int(b) << ' ';
    std::cout << std::dec << '\n';
}

int main() {
    uint8_t buffer[] = {0x10, 0x20, 0x30};
    std::vector<uint8_t> v = {1,2,3,4};
    std::array<uint8_t, 3> a = {9,8,7};

    print_bytes(buffer);             // 从内置数组构造
    print_bytes(v);                  // 从 vector 构造
    print_bytes(a);                  // 从 std::array 构造
    print_bytes({v.data(), 2});      // 从 pointer + size 构造
}

print_bytesstd::span<const uint8_t> 接收输入:既说明了不修改内容,又接受多种容器来源,调用方无需拷贝数据。


动态与静态 extent

std::span 有两种形态:

  • std::span<T>(或 std::span<T, std::dynamic_extent>):运行时大小;
  • std::span<T, N>:编译期固定元素数 N(称为静态 extent)。

示例:

int arr[4];
std::span<int, 4> s_fixed(arr);      // 只有长度为 4 的数组能绑定
std::span<int> s_dyn(arr, 4);        // 任意长度,运行时记录

静态 Extent 可以在某些场景下启用额外的编译期检查或优化,但在嵌入式中,动态 extent 更常用(因为 buffer 长度常由运行时决定)。


有用的成员函数

s.size();          // 元素个数
s.size_bytes();    // 字节数(注意!元素个数 * sizeof(T))
s.data();          // 指向首元素的指针(可能为 nullptr 当 size()==0)
s.empty();
s.front(), s.back();
s[i];              // 下标,不做运行时检查(与 operator[] 语义一致)
s.subspan(offset, count);   // 切片,返回新的 span(仍为 non-owning)
s.first(n), s.last(n);     // 前 n 个或后 n 个元素视图
std::as_bytes(s);          // 将 span<T> 视为 span<const std::byte>
std::as_writable_bytes(s); // 视为 span<std::byte>(当 T 可写时)

注意:operator[] 不检查越界;如果需要边界检查,自行用 at-like wrapper 或在调试时加断言。


进阶示例:subspan 与字节操作

#include <span>
#include <cstddef> // for std::byte

void recv_packet(std::span<uint8_t> buffer) {
    if (buffer.size() < 4) return;
    auto header = buffer.first(4);
    uint16_t len = header[2] | (header[3] << 8);

    if (buffer.size() < 4 + len) return;
    auto payload = buffer.subspan(4, len);

    // 把 payload 当作字节流传给 CRC 函数
    auto bytes = std::as_bytes(payload);
    // crc_check(bytes.data(), bytes.size()); // 示例:调用检验函数
}

这种把整体 buffer 切片成 header/payload 的写法尤其适合嵌入式协议解析,简洁而安全(只要你保证传进来的 buffer 有效)。


当做函数参数的最佳实践

把 API 设计成接收 std::span 有几个好处:

  • 调用者可以传入数组、std::arraystd::vector 或裸指针+长度;
  • 函数签名清楚地表达"这是一个视图(可能只读)";
  • 函数内不需要 template 泛型来支持各种容器。

示例:

void process(std::span<const int> data); // 明确:不修改数据
void mutate(std::span<int> data);         // 明确:会修改数据

这比写 template<class Container> void process(const Container& c) 更直观,也避免了不必要的编译膨胀。


常见坑

  1. 悬垂视图:最常见错误。不要把 std::span 绑定到局部 std::vectordata() 并把它返回给调用者:
std::span<int> bad() {
    std::vector<int> v = {1,2,3};
    return v; // ❌ v 被销毁,返回的 span 悬垂
}
  1. 以为有所有权:span 不持有内存,不会析构或释放。若需要所有权,用 std::vectorunique_ptr 等。

  2. 不恰当的字节视图std::as_bytes 返回 span<const std::byte>,用于只读字节访问;as_writable_bytes 仅在底层可写时使用。

  3. 越界访问operator[] 不检查边界。必要时做显式检查或使用调试断言。

  4. 不是以 null 结尾的字符串std::span<char> 不是 C 字符串,不保证以 '\0' 结尾。处理字符串请用 std::string_view 或明确长度处理。


std::string_view 的对比

  • std::string_view 是专门为字符序列设计的(只读视图),并带有字符串语义(常用于文本)。
  • std::span<char>/std::span<std::byte> 通用于任意元素类型,包括可写情况。 在处理二进制协议/缓冲区时,std::span 更合适;处理不可变文本时,用 string_view 更语义化。

嵌入式场景快速举例

  • DMA 回调把数据放进固定 buffer,回调把 std::span 传给处理函数,无需拷贝。
  • 从 Flash 读出数据到缓冲区,然后用 std::span 切片解析头和块。
  • 在中断或实时路径中传递小段数据,span 的拷贝开销极低。

代码小贴士

  1. 将函数参数写成 std::span<const T>,以表达只读意图。
  2. 若想允许传入大小为 N 的 buffer,但不更改逻辑,可接受 std::span<T, N>(静态 extent)。
  3. 使用 subspan, first, last 构造子视图,而非手动计算指针偏移。
  4. 在公共 API 文档里明确说明:span 不负责生命周期管理

速查 API

sstd::span<T>

  • s.size(), s.size_bytes(), s.data(), s.empty()
  • s[i](无边界检查)、s.front()s.back()
  • s.begin(), s.end()(支持范围 for)
  • s.subspan(offset, count), s.first(n), s.last(n)
  • std::as_bytes(s)std::as_writable_bytes(s)
查看完整可编译示例
// basic_usage.cpp - std::span 基本用法演示
#include <span>
#include <vector>
#include <array>
#include <iostream>

// 接受多种容器来源的统一函数接口
void print_bytes(std::span<const uint8_t> s) {
    for (auto b : s) std::cout << std::hex << int(b) << ' ';
    std::cout << std::dec << '\n';
}

// 演示可写 span
void fill_with_value(std::span<uint8_t> s, uint8_t value) {
    for (auto& b : s) {
        b = value;
    }
}

int main() {
    // 从内置数组构造
    uint8_t buffer[] = {0x10, 0x20, 0x30};
    std::cout << "From C array: ";
    print_bytes(buffer);

    // 从 vector 构造
    std::vector<uint8_t> v = {1, 2, 3, 4};
    std::cout << "From vector: ";
    print_bytes(v);

    // 从 std::array 构造
    std::array<uint8_t, 3> a = {9, 8, 7};
    std::cout << "From std::array: ";
    print_bytes(a);

    // 从 pointer + size 构造
    std::cout << "From ptr+size (first 2 of vector): ";
    print_bytes({v.data(), 2});

    // 演示可写 span
    std::array<uint8_t, 5> mutable_array{};
    fill_with_value(mutable_array, 0x42);
    std::cout << "After fill with 0x42: ";
    print_bytes(mutable_array);

    // 演示 span 的成员函数
    std::span<const uint8_t> s = buffer;
    std::cout << "\nspan member functions:\n";
    std::cout << "size(): " << s.size() << '\n';
    std::cout << "size_bytes(): " << s.size_bytes() << '\n';
    std::cout << "data(): " << static_cast<const void*>(s.data()) << '\n';
    std::cout << "empty(): " << s.empty() << '\n';
    std::cout << "front(): " << int(s.front()) << '\n';
    std::cout << "back(): " << int(s.back()) << '\n';

    return 0;
}
查看更多示例:静态 extent、切片、协议解析等
// static_extent.cpp - 静态 vs 动态 extent 演示
#include <span>
#include <array>
#include <iostream>

void process_dynamic(std::span<int> s) {
    std::cout << "Dynamic extent span, size: " << s.size() << '\n';
    for (auto& v : s) {
        std::cout << v << ' ';
    }
    std::cout << '\n';
}

// 编译期固定大小 - 更强的类型安全
void process_static_4(std::span<int, 4> s) {
    std::cout << "Static extent(4) span, size: " << s.size() << '\n';
    for (auto& v : s) {
        std::cout << v << ' ';
    }
    std::cout << '\n';
}

int main() {
    int arr[4] = {1, 2, 3, 4};

    // 动态 extent - 可以接受任意大小
    std::span<int> s_dyn(arr);
    process_dynamic(s_dyn);

    // 静态 extent - 编译期检查大小
    std::span<int, 4> s_fixed(arr);
    process_static_4(s_fixed);

    // 编译期大小检查演示
    std::cout << "\nExtent values:\n";
    std::cout << "s_dyn.extent: " << s_dyn.extent << '\n';     // std::dynamic_extent
    std::cout << "s_fixed.extent: " << s_fixed.extent << '\n';  // 4

    // 类型安全演示
    int arr2[6] = {1, 2, 3, 4, 5, 6};
    std::span<int, 6> s_fixed6(arr2);

    // 以下代码会在编译期报错(取消注释以查看错误):
    // process_static_4(s_fixed6);  // 错误:大小不匹配

    // 但可以用 first<N>() 获取静态大小的子视图
    auto first_4 = s_fixed6.first<4>();
    process_static_4(first_4);

    return 0;
}
// subspan_example.cpp - span 切片操作演示
#include <span>
#include <array>
#include <iostream>

int main() {
    std::array<int, 10> data = {0, 1, 2, 3, 4, 5, 6, 7, 8, 9};
    std::span<int> s = data;

    std::cout << "Original data: ";
    for (auto v : s) std::cout << v << ' ';
    std::cout << '\n';

    // first(n) - 获取前 n 个元素的视图
    auto first_three = s.first(3);
    std::cout << "First 3: ";
    for (auto v : first_three) std::cout << v << ' ';
    std::cout << '\n';

    // first<N>() - 编译期获取前 N 个元素
    auto first_five = s.first<5>();
    std::cout << "First 5 (compile-time): ";
    for (auto v : first_five) std::cout << v << ' ';
    std::cout << '\n';

    // last(n) - 获取后 n 个元素的视图
    auto last_three = s.last(3);
    std::cout << "Last 3: ";
    for (auto v : last_three) std::cout << v << ' ';
    std::cout << '\n';

    // subspan(offset, count) - 获取从 offset 开始的 count 个元素
    auto middle = s.subspan(3, 4);
    std::cout << "Subspan [3:7]: ";
    for (auto v : middle) std::cout << v << ' ';
    std::cout << '\n';

    // subspan<Offset, Count>() - 编译期切片
    auto middle_static = s.subspan<2, 5>();
    std::cout << "Subspan <2,5> (compile-time): ";
    for (auto v : middle_static) std::cout << v << ' ';
    std::cout << '\n';

    // 修改视图会修改原始数据
    first_three[0] = 99;
    std::cout << "\nAfter modifying first_three[0] to 99:\n";
    std::cout << "Original data: ";
    for (auto v : data) std::cout << v << ' ';
    std::cout << '\n';

    return 0;
}
// packet_parsing.cpp - 实际嵌入式场景:协议包解析
#include <span>
#include <cstdint>
#include <iostream>

// 模拟 CRC 校验函数
bool crc_check(const uint8_t* data, size_t length) {
    // 简化:假设 CRC 总是正确
    (void)data;
    (void)length;
    return true;
}

// 解析数据包
void recv_packet(std::span<uint8_t> buffer) {
    std::cout << "Received " << buffer.size() << " bytes\n";

    // 检查最小包长度
    if (buffer.size() < 4) {
        std::cout << "Packet too short\n";
        return;
    }

    // 解析头部(前4字节)
    auto header = buffer.first(4);
    uint8_t msg_id = header[0];
    uint8_t flags = header[1];
    uint16_t length = header[2] | (header[3] << 8);

    std::cout << "Header: ID=" << int(msg_id)
              << ", Flags=" << int(flags)
              << ", Length=" << length << '\n';

    // 检查长度字段是否合法
    if (buffer.size() < 4 + length) {
        std::cout << "Length field exceeds buffer size\n";
        return;
    }

    // 提取 payload
    auto payload = buffer.subspan(4, length);
    std::cout << "Payload: ";
    for (auto b : payload) {
        std::cout << std::hex << int(b) << ' ';
    }
    std::cout << std::dec << '\n';

    // CRC 校验(把 payload 当作字节流)
    auto bytes = std::as_bytes(payload);
    if (crc_check(static_cast<const uint8_t*>(bytes.data()), bytes.size())) {
        std::cout << "CRC: OK\n";
    } else {
        std::cout << "CRC: FAILED\n";
    }
}

int main() {
    // 模拟接收到的数据包
    // 格式: [MSG_ID][FLAGS][LEN_L][LEN_H][PAYLOAD...]
    std::array<uint8_t, 16> packet = {
        0x01,        // Message ID
        0x00,        // Flags
        0x08, 0x00,  // Length = 8
        // Payload (8 bytes)
        0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08,
        // Extra data (should be ignored)
        0xFF, 0xFF, 0xFF, 0xFF
    };

    std::cout << "=== Parsing valid packet ===\n";
    recv_packet(packet);

    // 测试短包
    std::array<uint8_t, 2> short_packet = {0x01, 0x00};
    std::cout << "\n=== Parsing short packet ===\n";
    recv_packet(short_packet);

    // 测试长度字段不匹配
    std::array<uint8_t, 10> bad_length_packet = {
        0x01, 0x00, 0x20, 0x00,  // Claims length=32
        0x01, 0x02, 0x03, 0x04, 0x05, 0x06
    };
    std::cout << "\n=== Parsing packet with bad length ===\n";
    recv_packet(bad_length_packet);

    return 0;
}
// bytes_view.cpp - 字节视图演示 (as_bytes, as_writable_bytes)
#include <span>
#include <array>
#include <cstdint>
#include <iostream>

void print_bytes(std::string_view label, std::span<const std::byte> bytes) {
    std::cout << label << ": ";
    for (auto b : bytes) {
        std::cout << std::hex << std::setw(2) << std::setfill('0')
                  << static_cast<int>(b) << ' ';
    }
    std::cout << std::dec << '\n';
}

int main() {
    // 演示 as_bytes - 将任意类型 span 转换为字节视图(只读)
    std::array<uint32_t, 4> data = {0x12345678, 0x9ABCDEF0, 0x11223344, 0x55667788};

    std::span<uint32_t> data_span = data;
    auto byte_view = std::as_bytes(data_span);

    std::cout << "Original uint32_t values:\n";
    for (auto v : data) {
        std::cout << "0x" << std::hex << v << ' ';
    }
    std::cout << std::dec << "\n\n";

    std::cout << "As bytes (little-endian):\n";
    print_bytes("Byte view", byte_view);

    // 演示 as_writable_bytes - 可写字节视图
    std::array<uint8_t, 8> buffer = {};
    std::span<uint8_t> buffer_span = buffer;

    auto writable_bytes = std::as_writable_bytes(buffer_span);
    std::cout << "\nBefore writing:\n";
    print_bytes("Buffer", std::as_bytes(buffer_span));

    // 写入一些字节
    for (size_t i = 0; i < writable_bytes.size(); ++i) {
        writable_bytes[i] = static_cast<std::byte>(i * 2 + 1);
    }

    std::cout << "\nAfter writing:\n";
    print_bytes("Buffer", std::as_bytes(buffer_span));

    // 演示从 uint8_t span 转换
    std::array<uint8_t, 5> raw_data = {0x10, 0x20, 0x30, 0x40, 0x50};
    std::span<const uint8_t> raw_span = raw_data;
    auto raw_bytes = std::as_bytes(raw_span);

    std::cout << "\nRaw uint8_t data as bytes:\n";
    print_bytes("Raw", raw_bytes);

    // 注意:对于已经是 uint8_t 的 span,as_bytes 不会改变表示
    std::cout << "\nNote: For uint8_t span, as_bytes() doesn't change representation\n";
    std::cout << "raw_span.size(): " << raw_span.size() << '\n';
    std::cout << "raw_bytes.size(): " << raw_bytes.size() << '\n';

    return 0;
}
// function_parameter.cpp - 函数参数最佳实践演示
#include <span>
#include <vector>
#include <array>
#include <iostream>

// 推荐方式:明确表达不修改数据的意图
void process(std::span<const int> data) {
    std::cout << "Processing " << data.size() << " elements: ";
    for (auto v : data) {
        std::cout << v << ' ';
    }
    std::cout << '\n';
}

// 明确表达会修改数据的意图
void mutate(std::span<int> data) {
    std::cout << "Mutating " << data.size() << " elements\n";
    for (auto& v : data) {
        v *= 2;
    }
}

// 对比:传统 C 风格 API(不推荐)
void old_c_style_api(const int* data, size_t length) {
    std::cout << "Old style: " << length << " elements: ";
    for (size_t i = 0; i < length; ++i) {
        std::cout << data[i] << ' ';
    }
    std::cout << '\n';
}

// 对比:模板泛型(会导致代码膨胀)
template<class Container>
void template_api(const Container& c) {
    std::cout << "Template API: " << c.size() << " elements: ";
    for (auto v : c) {
        std::cout << v << ' ';
    }
    std::cout << '\n';
}

int main() {
    std::vector<int> vec = {1, 2, 3, 4, 5};
    std::array<int, 4> arr = {10, 20, 30, 40};
    int c_array[] = {100, 200, 300};

    std::cout << "=== Using std::span API (recommended) ===\n";
    process(vec);
    process(arr);
    process(c_array);

    std::cout << "\n=== After mutation ===\n";
    mutate(vec);
    process(vec);

    std::cout << "\n=== Using old C style API ===\n";
    old_c_style_api(vec.data(), vec.size());
    old_c_style_api(arr.data(), arr.size());

    std::cout << "\n=== Using template API (code bloat) ===\n";
    template_api(vec);
    template_api(arr);

    std::cout << "\n=== Advantages of std::span ===\n";
    std::cout << "- Single function implementation (no code bloat)\n";
    std::cout << "- Type and size information bundled together\n";
    std::cout << "- No copying of data\n";
    std::cout << "- Clear const-correctness\n";

    return 0;
}