Article
C++のテンプレート肥大化を抑える:std::spanで型依存の境界を小さくする
型に依存する処理がわずかでも、大きなテンプレート関数は型ごとに展開されます。std::spanを使った共通処理の分離と、寿命・評価順序・最適化後のコードサイズから適用を判断する方法を整理します。
Share
こはるの読みどころ
長い関数のうち、本当に型が必要なのはどこかな?共通処理へ渡すデータの形と寿命を見直すと、分ける場所が見えてくるよ。

C++で似た処理をテンプレートにまとめると、ソースコードの重複は減らせます。ただ、同じように実行ファイルの重複も減るとは限りません。長い関数の中で型が必要なのはほんの一部分、という場合にこの違いが効いてきます。
2026年9月25日にISO C++ Blogが紹介したのは、型に依存する部分を切り分けてコードの肥大化を抑える考え方です。入口の柔軟さを残したまま、どこまで処理を共有できるのでしょうか。
その境界を考える道具として、C++20の std::span が役立ちます。共通化できる情報と、移動すると挙動が変わる処理を分けて考えると、適用すべき場所が見えてきます。
ラムダ式の型が増えると、共通処理まで展開される
関数テンプレートは、異なるテンプレート引数に対して別の特殊化を作ります。特にラムダを受け取るテンプレートでは、よく似た処理を書く場合でも型が増え得ます。C++作業草案のクロージャ型の規定では、ラムダ式のクロージャ型は固有の名前のないクラス型です。
ここで区別したいのは、ラムダ式を書く場所と実行回数です。同じラムダ式をループで繰り返し評価しても、そのたびに新しい型が作られるわけではありません。別々のラムダ式を同じ関数テンプレートへ渡す場合に、型の違いが展開を増やす要因になります。
一方、特殊化の数と、最終的に残る機械語の数は同じとは限りません。MSVCのリンカーには、同一のCOMDATをまとめる /OPT:ICF があります。ただし、これは同一の生成結果を統合する仕組みなので、処理の意味が似ているだけで共有されるとは考えられません。MSVCの最適化オプション
そこで、生成後の統合だけを当てにせず、関数の境界で共通処理を表現する方法を考えます。
std::spanでコンテナ型と要素数を共通処理から切り離す
型ごとに必要な情報を取り出し、それを受け取る大きな処理を非テンプレート関数にすると、入口だけを小さなテンプレートにできます。Raymond Chenの型依存部分を分離する解説は、列情報の取得を入口へ移し、共通のワーカーへ渡す形を示しています。
std::span は、連続して配置された要素列を見るための型です。C配列、std::array、通常の std::vector などの要素を、所有権を移さずに扱えます。<span> はC++20から利用でき、MSVCでは /std:c++20 以降が必要です。Microsoft Learnのspan解説
たとえば、読み取り専用の列情報を扱うワーカーの引数を std::span<const Column> にそろえる設計を考えます。次は、要素型がすべて Column で、参照先が有効な場合の対応です。
| 呼び出し側の保存形式 | ワーカーが受け取る型 | 要素数の扱い |
|---|---|---|
std::array<Column, 3> |
std::span<const Column> |
実行時の値として3を渡す |
std::array<Column, 8> |
std::span<const Column> |
実行時の値として8を渡す |
std::vector<Column> |
std::span<const Column> |
現在の要素数を渡す |
長さを省略したspanは std::dynamic_extent を使います。コンテナの種類や配列長をワーカーの型から外せるので、列数の異なる入力を同じ関数で処理できるわけですね。
反対に、ワーカーを要素数 N のテンプレートにして std::span<const Column, N> を受け取れば、長さごとの型依存が残ります。どの情報をコンパイル時に保持し、どこから実行時の値として渡すかが設計上の選択になります。spanのテンプレート定義と変換条件
この境界が共有するのは、同じ要素型の連続したデータです。要素型まで異なる入力や、任意のラムダの振る舞いを、そのままspanへ置き換えられるわけではありません。
列情報を先に渡すなら、取得順序と寿命も変わる
共通ワーカーへ必要な値を渡すには、その値を呼び出し前に用意します。従来は「準備処理、列情報の取得、列の処理」という順番だったものが、「列情報の取得、ワーカー内の準備処理、列の処理」に変わる可能性があります。この評価順序の変化はChenの解説でも指摘されています。
たとえば、列情報を作る処理にログ出力や外部状態の参照があれば、移動によって観測できる挙動が変わり得ます。準備処理が例外で終わる場合にも、以前は実行されなかった取得処理が先に走ります。これは適用先で調べるべき条件であり、単なる関数分割だけでは同値性を判断できません。
もう一つの条件は、spanがデータを所有しないことです。spanの仕様は、記憶域の所有者が別に存在することと、元のポインターを無効化する操作がspan経由の参照にも影響することを定めています。呼び出し元で作ったvectorを参照するなら、ワーカーが読み終えるまで所有者と記憶域を有効に保つ必要があります。
同期的な呼び出し中だけ使う設計と、spanを保存して後から使う設計では、必要な寿命が異なります。const を付けても所有者の寿命は延びません。
非テンプレート化の効果は、最適化後のサイズと速度で判断する
ソース上でワーカーを一つにしても、インライン展開によって呼び出し先の処理が再び取り込まれることがあります。MSVC固有の __declspec(noinline) はインライン化を抑制する指定ですが、共通化した関数すべてに付ける前提にはせず、生成結果を見るための選択肢として扱いたいところです。MSVCのnoinline解説
適用を検討するなら、まず重複が疑われる大きなテンプレートを一つ選び、変更前後を同じコンパイラー、最適化設定、対象アーキテクチャで比較する方法が考えられます。コード領域のサイズと対象処理の実行時間を見れば、共有によるサイズの変化と、呼び出し境界を変えた結果を別々に判断できます。これは検証の提案であり、一定の削減率や高速化を保証するものではありません。
MSVCでは /DEBUG の有無で /OPT:ICF の既定値が変わるため、Debugビルドだけの比較を配布用ビルドへ一般化しないようにします。/VERBOSE では統合された関数も調べられます。リンカー最適化と診断出力
入口の柔軟さを残して共有できるのは、具体的な型を知らなくても実行できる処理です。その境界を小さくし、取得順序と寿命を保ち、配布用の設定で効果を確かめる。この順番で考えると、テンプレートの便利さを残しながら、実行ファイルに残る重複へ対処できます。
出典
- Title: Reducing C++ template bloat by factoring out type-dependent portions of the function -- Raymond Chen
- URL: https://isocpp.org//blog/2026/09/reducing-cpp-template-bloat-by-factoring-out-type-dependent-portions-of-the
Share
Related Articles
カテゴリやタグが近い記事を続けて読めるように並べています。




