Ir para o conteúdo

Última atualização:2026-10-12

Design e armadilhas do std::span no C++20

0.Motivação

No código C/C++ tradicional, geralmente usamos um ponteiro + tamanho para acessar um array, por exemplo:

void print(int* arr, int sz) {
    for (int i = 0; i < sz; i++) {
        // todo
    }
}

O span pode simplificar essa operação, tornando-a:

void print(std::span<int> arr) {
    for (auto x : arr) {
        // todo
    }
}

Então, o que é exatamente um span, quais são seus cenários de uso e quais são suas armadilhas? Esta seção explora isso em detalhes.

1. O que é span?

No C++20, span é um contêiner leve, do tipo range, que fornece acesso a um trecho de memória contígua. Como view (visão), o span não possui seus dados; é apenas uma referência simples aos dados, usado com frequência como substituto dos arrays estilo C ou ponteiros tradicionais. Ele pode acomodar arrays e contêineres padrão de qualquer tipo, como std::vector, oferecendo uma forma mais segura e moderna de tratar esses dados.

Alguns pontos-chave:

  • Memória contígua
  • Não possui os dados
  • Referência

O código-fonte de span está em gcc, em libstdc++-v3/include/std/span.

template <typename _Type, size_t _Extent = dynamic_extent>
class span {};

O tipo span contém dois parâmetros importantes:

  • _Type: o tipo do elemento.
  • Extent: o tamanho do array. Se for dynamic_extent (o valor padrão), significa que o tamanho da view pode variar dinamicamente.

2. span dinâmico e estático

O span tem dois modos: estático e dinâmico.

2.1 span estático

O span estático determina seu tamanho em tempo de compilação; seu Extent é um valor constante. Ao usar um span estático, precisamos conhecer o tamanho dos dados em tempo de compilação.

Por exemplo:

int arr[5] = {1, 2, 3, 4, 5};
std::span<int, 5> s(arr);  // 静态 span,大小为 5

O tamanho do span estático é fixado em tempo de compilação; portanto, é possível aproveitar o compilador para algumas otimizações, como a omissão de verificação de limites. Como Extent é uma constante, o span estático não pode ter o tamanho modificado em tempo de execução.

2.2 span dinâmico

O tamanho do span dinâmico é determinado em tempo de execução; Extent assume por padrão dynamic_extent. Esse tipo de span não exige que o tamanho seja conhecido em tempo de compilação e pode se adaptar a arrays ou contêineres de qualquer tamanho.

Por exemplo:

int arr[5] = {1, 2, 3, 4, 5};
std::span<int> s(arr);  // 动态 span,大小在运行时确定

O span dinâmico é adequado para situações em que o tamanho não pode ser determinado em tempo de compilação, oferecendo maior flexibilidade.

Na implementação do código-fonte do gcc, isso é bem simples: por padrão é um inteiro muito grande (o unsigned de -1); o cálculo real acontece na construção.

inline constexpr size_t dynamic_extent = static_cast<size_t>(-1);

Por exemplo: aqui usa-se ranges::size para definir o tamanho interno; veja o código abaixo.

std::vector<int> data{1, 2, 3};
std::span<int> s(data);

Define o tamanho dinamicamente.

span(ranges::data(__range), ranges::size(__range))

3. subspan

O span fornece uma função-membro muito útil, subspan, que nos permite extrair uma subvisão de um span existente sem copiar dados. Ela é muito adequada para evitar overhead de memória desnecessário ao tratar grandes volumes de dados.

O método subspan() tem duas sobrecargas:

  1. Uma aceita __offset e __count como parâmetros, em que __offset é a posição inicial e __count é o tamanho do subintervalo (por padrão, dynamic_extent, ou seja, até o fim do span original).
  2. Outra não tem parâmetros e é implementada com base em templates.
constexpr auto
subspan() const noexcept
-> span<element_type, _S_subspan_extent<_Offset, _Count>()> {}

template<size_t _Offset, size_t _Count = dynamic_extent>
[[nodiscard]]
constexpr auto
subspan() const noexcept
-> span<element_type, _S_subspan_extent<_Offset, _Count>()> {}

Exemplo 1:

std::span<int> s(arr);  
std::span<int> sub = s.subspan(1, 3);

Exemplo 2:

std::span<int> s(arr);  
auto sub = s.subspan<2, 2>();

4. Armadilhas de uso

Embora o span seja uma ferramenta muito útil, há algumas armadilhas comuns a observar no uso:

4.1 Não deixe o span referenciar memória já liberada

O span em si não possui os dados; é apenas uma referência a eles. Portanto, se a memória referenciada pelo span for liberada (por exemplo, o array original é destruído ou sai do escopo), acessar esse span resultará em comportamento indefinido.

Por exemplo:

std::span<int> create_span() {
    int arr[5] = {1, 2, 3, 4, 5};
    return std::span<int>(arr, 5);
}

// 错误:返回的 span 引用了栈上临时变量,函数返回后 arr 被销毁
auto s = create_span();

Para resolver esse problema, garanta que os dados referenciados pelo span sejam válidos durante todo o seu tempo de vida. Considere manter os dados em memória alocada dinamicamente, ou assegurar que o tempo de vida dos dados coincida com o do span.

4.2 Problemas ao alterar o tamanho do array subjacente

A referência do span aos dados subjacentes é direta, e não uma cópia. Quando os dados subjacentes mudam, o span em si não percebe essas mudanças. Assim, ao alterar o tamanho do array ou contêiner subjacente (por exemplo, por meio de realocação de memória), o span original pode se tornar inválido.

std::vector<int> vec = {1, 2, 3, 4, 5};
std::span<int> s(vec);  // s 引用 vec 的数据
// 改变底层容器的大小
vec.push_back(6);  // vec 大小变为 6

// 这里 s 仍然引用 vec 的数据,但 vec 可能已经重新分配了内存
// 这可能导致 s 引用失效或未定义行为

Tópicos relacionados

Comentários