コンテンツにスキップ

最終更新:2026-10-12

C++20 std::span の設計と落とし穴

0.きっかけ

従来の C/C++ コードでは、配列にアクセスするためにポインタ+サイズを使うことが多いでしょう。例えば:

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

span はこの操作を簡略化し、次のようにできます:

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

では、span とは一体何なのか、その使用シーンと落とし穴にはどんなものがあるのか?本節で詳しく掘り下げます。

1. span とは?

C++20 において、span は軽量な範囲タイプのコンテナで、**連続したメモリ領域**へのアクセスを提供します。span はビュー(view)としてデータを所有せず、データへの単純な参照であり、従来の C スタイル配列やポインタの代替としてよく使われます。任意の型の配列や std::vector のような標準コンテナを格納でき、これらのデータをより安全でモダンな方法で扱う手段を提供します。

重要なポイントは次の通りです:

  • 連続したメモリ
  • データを所有しない
  • 参照

span のソースコードは gcc の libstdc++-v3/include/std/span にあります。

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

span 型には2つの重要なパラメータがあります:

  • _Type:要素の型。
  • Extent:配列のサイズ。dynamic_extent(デフォルト値)の場合、ビューのサイズが動的に変化できることを示します。

2. span の動的と静的

span には2つのモードがあります:静的と動的です。

2.1 静的 span

静的 span はコンパイル時にサイズが確定し、その Extent は定数です。静的 span を使う場合、コンパイル時にデータのサイズを知っている必要があります。

例えば:

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

静的 span のサイズはコンパイル時に固定されるため、境界チェックの省略などのコンパイラ最適化を利用できます。ただし Extent が定数のため、静的 span は実行時にサイズを変更できません。

2.2 動的 span

動的 span のサイズは実行時に確定し、Extent はデフォルトで dynamic_extent です。このタイプの span はコンパイル時にサイズを確定する必要がなく、任意のサイズの配列やコンテナに適応できます。

例えば:

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

動的 span は、コンパイル時にサイズを確定できないケースに適しており、より大きな柔軟性を提供します。

gcc のソースでの実装は比較的単純で、デフォルトは非常に大きな整数(-1 の符号なし数)で、実際の計算は構築時に行われます。

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

例えば:ここでは ranges::size を使って内部サイズを設定します。詳細は以下のコードをご覧ください。

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

動的サイズを設定します。

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

3. subspan

span は非常に有用なメンバ関数 subspan を提供します。これにより、既存の span からサブビューを、データをコピーせずに抽出できます。大きなデータセットを扱う際に不要なメモリオーバーヘッドを避けるのに非常に適しています。

subspan() メソッドには2つのオーバーロードがあります:

  1. __offset と __count を引数に取るもの。__offset は開始位置、__count はサブ範囲のサイズです(デフォルトは dynamic_extent、つまり元の span の末尾まで)。
  2. 引数を取らず、テンプレートで実装されたもの。
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>()> {}

例1:

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

例2:

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

4. 使用上の落とし穴

span は非常に有用なツールですが、使用時には注意すべきよくある落とし穴もあります:

4.1 解放済みメモリを span に参照させない

span 自体はデータを所有せず、データへの参照にすぎません。したがって、span が参照するメモリが解放された場合(例えば元の配列が破棄されたりスコープを抜けたりした場合)、その span にアクセスすると未定義動作になります。

例えば:

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();

この問題を解決するには、span が参照するデータがその生存期間中有効であることを保証します。データを動的に割り当てたメモリに保持するか、データの生存期間と span の生存期間を一致させることを検討してください。

4.2 底層配列のサイズを変更する際の問題

span の底層データへの参照は直接であり、コピーではありません。底層のデータが変化しても、span 自体はその変化を感知しません。そのため、底層の配列やコンテナのサイズを変更すると(例えばメモリの再割り当てにより)、元の span は無効になる可能性があります。

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 引用失效或未定义行为

関連トピック

コメント