**标题:** 如何在 C++20 中安全地使用 `std::span` 与容器的生命周期?

正文:

在 C++20 中,std::span 提供了一个轻量级、无所有权的视图,用来表示一段连续内存。它可以用来替代传统的裸指针和长度对,但使用时必须谨慎,尤其是与容器的生命周期相关。以下是关于安全使用 std::span 的关键点和实战建议。


1. std::span 的基本定义

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

std::span <int> make_span(std::vector<int>& v) {
    return std::span <int>(v.data(), v.size());
}

std::span 本身不持有数据,它只包含:

  • 一个指向起始元素的指针 (T*)
  • 一个表示长度的 size_t

因此,std::span 并不管理对象的生命周期。


2. 生命周期与所有权

当你从一个容器返回 std::span 时,需要确保返回的 std::span 只在容器有效时使用。常见错误示例:

std::span <int> bad_span() {
    std::vector <int> local_vec{1, 2, 3};
    return std::span <int>(local_vec); // UB: local_vec destroyed at end of function
}

在此,std::span 指向已被销毁的内存,导致未定义行为。

正确做法:仅返回对外部已存在容器的 std::span,或将 std::span 用作函数参数(传递引用而非所有权)。


3. 作为函数参数的安全模式

void process(std::span<const int> data) {
    // 只读访问
    for (auto v : data) {
        std::cout << v << ' ';
    }
    std::cout << '\n';
}
  • 传递 const:如果不需要修改,使用 const 可以防止意外写入。
  • **传递 `std::span `**:若需要修改,确保调用者传入的容器在函数内部保持生命周期。

使用时,推荐把容器放在外部:

std::vector <int> numbers{10, 20, 30};
process(numbers);          // 隐式转换为 std::span<const int>

4. 与 std::array、C-风格数组配合

std::array<int, 5> arr{ {1, 2, 3, 4, 5} };
process(arr);               // 同样支持

int c_arr[4] = { 4, 5, 6, 7 };
process(std::span <int>(c_arr, 4)); // 需要显式指定长度

由于 std::array 的大小在编译时已知,std::span 的使用更安全。C-风格数组必须手动传递长度,错误的长度会导致越界。


5. 与 std::vector 的扩展使用

  • 子视图:使用 std::span::subspan
std::vector <int> vec{1,2,3,4,5,6,7,8,9,10};
std::span <int> full(vec);
std::span <int> mid = full.subspan(3, 4);  // [4,5,6,7]
  • 连续性检查:在容器插入/删除时,原 std::span 可能失效。若需要保持引用,请使用 std::vector::reservestd::list(不支持 std::span)。

6. 防止悬挂 std::span

  • 不可在 std::span 生命周期内修改容器:如 push_backclearresize 等会重新分配内存,导致 std::span 指针失效。
  • 使用 std::span::data()const 版本:如果你不需要写入,使用 const 可以防止误操作。

7. 实战示例:安全地批量更新

假设你有一个数值矩阵,需要按行批量更新:

void batch_update(std::vector<std::vector<int>>& matrix,
                  const std::vector<std::size_t>& rows,
                  const std::vector <int>& new_values)
{
    // 计算总长度
    std::size_t total = 0;
    for (auto r : rows) total += matrix[r].size();

    if (total != new_values.size())
        throw std::invalid_argument("size mismatch");

    // 创建一个连续视图
    std::span <int> values(new_values.data(), new_values.size());

    std::size_t idx = 0;
    for (auto r : rows) {
        auto row_span = std::span <int>(matrix[r].data(), matrix[r].size());
        std::copy(values.subspan(idx, row_span.size()).begin(),
                  values.subspan(idx, row_span.size()).end(),
                  row_span.begin());
        idx += row_span.size();
    }
}
  • matrix 必须保持不变(不执行 reserveclear 等)才能安全使用 std::span
  • 通过 subspan 实现对每行的局部更新,避免拷贝整行。

8. 结论

  • std::span 是一个强大的工具,但它不管理生命周期,使用时必须确保被视图的底层数据在整个使用期间保持有效。
  • 在设计接口时,优先将 std::span 作为参数(而非返回值),并使用 constmutable 版本根据需求控制访问。
  • 对于会导致容器重新分配的操作,需在使用 std::span 前后避免或重新获取视图。

遵循上述规则,可以在 C++20 及以后版本中安全、高效地使用 std::span,充分发挥其轻量视图的优势。

发表评论