C++の可変長引数とは、関数やテンプレートに対して、決まった数ではなく任意の個数の引数を渡せる仕組みのことです。
通常の関数では、あらかじめ引数の数と型を決めておく必要があります。
しかし可変長引数を使うと、1個だけ渡す場合、3個渡す場合、型の異なる値を複数渡す場合など、より柔軟な関数設計ができます。
C++には可変長引数を扱う方法が複数あります。
代表的なものは、C言語由来の可変長引数、C++11以降で使える可変長テンプレート、そして同じ型の値を複数受け取る std::initializer_list です。
現代のC++では、基本的に可変長テンプレートを使うのが一般的です。
C言語由来の可変長引数は型安全性が低く、実装を誤ると未定義動作につながる可能性があるため、新しくC++のコードを書く場合には慎重に扱う必要があります。
C++で使われる主な可変長引数の種類
C形式の可変長引数
C形式の可変長引数は、C言語から引き継がれた古い仕組みです。
代表的な例としては、printf のような関数があります。
この方式では、関数に固定の引数をいくつか渡したあと、その後ろに任意の個数の引数を渡せます。
ただし、可変長部分の型情報は安全に管理されません。
そのため、関数側は「何個の引数が渡されたのか」「それぞれがどの型なのか」を、別の情報をもとに自分で判断する必要があります。
たとえば、最初の引数で個数を指定したり、フォーマット文字列で型を表したりします。
しかし、この情報が実際の引数と一致していない場合、プログラムは正しく動作しない可能性があります。
場合によっては未定義動作になります。
可変長テンプレート
可変長テンプレートは、C++11で導入された現代的な仕組みです。
可変長テンプレートを使うと、任意個数の型や値をテンプレートとして扱えます。
C形式の可変長引数とは異なり、コンパイラが型情報を保持できるため、型安全なコードを書きやすいという大きなメリットがあります。
たとえば、整数、浮動小数点数、文字列など、異なる型の値をまとめて受け取り、それぞれに対して処理を行うような関数を作れます。
現代C++で「可変長引数」といった場合、多くはこの可変長テンプレートを指します。
std::initializer_list
std::initializer_list は、同じ型の値を複数まとめて受け取るための仕組みです。
厳密には、C形式の可変長引数や可変長テンプレートとは異なります。
任意の型を自由に受け取るというより、同じ型、または共通の型として扱える値のリストを受け取る仕組みです。
たとえば、複数の整数、複数の文字列、複数の設定値などをまとめて渡したい場合に向いています。
そのため、std::initializer_list は「可変長引数そのもの」というより、「同じ型の値を可変長風に受け取る方法」と考えると分かりやすいです。
C形式の可変長引数の特徴
古いC互換の仕組み
C形式の可変長引数は、C言語との互換性を保つためにC++でも使える仕組みです。
ただし、C++らしい安全な抽象化とは相性がよくありません。
C++では型安全性やテンプレートによる静的チェックを重視できるため、新しいコードであえてC形式の可変長引数を使う場面は多くありません。
主に、古いCライブラリと連携する場合や、既存のC APIを扱う場合に使われます。
型安全ではない
C形式の可変長引数の最大の問題は、型安全ではないことです。
関数側は、可変長部分にどの型の値が渡されたかをコンパイラから直接教えてもらえません。
そのため、プログラマーが「この順番で、この型の値が渡されているはずだ」と想定して取り出す必要があります。
しかし、呼び出し側が想定と違う型を渡した場合、関数側はそれを正しく検出できないことがあります。
たとえば、整数として取り出すつもりなのに実際には小数が渡されていた場合、正常な値として扱えない可能性があります。
このような型の不一致は未定義動作につながることがあります。
引数の個数も自分で管理する必要がある
C形式の可変長引数では、引数の個数も自動では安全に管理されません。
そのため、呼び出し側と関数側の間で「何個の値を渡したのか」を正しく合わせる必要があります。
もし関数側が、実際より多くの引数があると思って読み取ろうとすると、本来存在しない値を読み取ることになります。
これも未定義動作の原因になります。
デフォルト引数昇格に注意が必要
C形式の可変長引数では、可変長部分に渡された値にデフォルト引数昇格が行われます。
たとえば、float は double に昇格されます。
また、char や short、bool などは int として扱われることがあります。
つまり、呼び出し側で渡した見た目の型と、関数側で取り出すべき型が必ずしも同じとは限りません。
この仕組みを理解していないと、正しく値を取り出せず、危険なコードになる可能性があります。
現代C++では基本的に避けた方がよい
C形式の可変長引数は、Cとの互換性が必要な場面では今でも使われます。
しかし、新しくC++のコードを書く場合には、基本的に避けた方がよいです。
理由は、型安全ではなく、引数の個数や型の管理をプログラマーが手作業で行う必要があるからです。
現代C++では、可変長テンプレートを使うことで、より安全で柔軟な設計ができます。
可変長テンプレートの特徴
現代C++で推奨される方法
可変長テンプレートは、C++で任意個数の引数を扱うための中心的な仕組みです。
任意個数の型をまとめて扱えるため、型が異なる複数の引数を安全に受け取ることができます。
C形式の可変長引数と違い、コンパイラが型情報を保持してくれるため、間違った型の使い方をコンパイル時に検出しやすくなります。
パラメータパックを使う
可変長テンプレートでは、複数の型や値の集まりを「パラメータパック」として扱います。
パラメータパックには、型の並びを表すものと、値の並びを表すものがあります。
たとえば、整数、文字列、小数のように異なる型の引数を渡した場合、可変長テンプレートではそれぞれの型をまとめて扱えます。
この仕組みによって、引数の個数や型が固定されていない関数を、型安全に作ることができます。
引数の個数をコンパイル時に把握できる
可変長テンプレートでは、渡された引数の個数をコンパイル時に把握できます。
C形式の可変長引数のように、別途「何個渡したか」を手動で伝える必要はありません。
これにより、引数の数を間違えて未定義動作になるリスクを大きく減らせます。
型情報を保持できる
可変長テンプレートの大きな強みは、引数の型情報を保持できることです。
C形式の可変長引数では、可変長部分の型情報が安全に扱われません。
一方、可変長テンプレートでは、コンパイラがそれぞれの引数の型を認識します。
そのため、型に応じた処理を行ったり、特定の条件を満たす型だけを受け取るように制限したりできます。
パック展開とは
パラメータパックを実際の引数列に展開する仕組み
可変長テンプレートでは、パラメータパックをそのまま使うだけではなく、必要に応じて展開します。
パック展開とは、複数の引数をまとめて保持しているパックを、実際の引数列として広げることです。
たとえば、3つの引数を受け取ったパックがある場合、それを別の関数にそのまま3つの引数として渡すことができます。
ラッパー関数でよく使われる
パック展開は、ラッパー関数や中継関数でよく使われます。
ある関数が任意個数の引数を受け取り、それを別の関数にそのまま渡すような場面です。
たとえば、オブジェクト生成関数、ログ出力関数、イベント通知関数、コールバック呼び出しなどで使われます。
畳み込み式とは
C++17で導入された便利な構文
畳み込み式は、C++17で導入された構文です。
可変長テンプレートで受け取った複数の引数に対して、同じ演算をまとめて適用できます。
たとえば、複数の値をすべて足す、すべての条件が真かどうかを判定する、すべての値を順番に出力する、といった処理に使えます。
再帰的なテンプレート処理を簡潔にできる
C++17以前は、可変長テンプレートの引数を順番に処理するために、再帰的な関数テンプレートを書くことがよくありました。
しかし、畳み込み式を使うと、そのような再帰処理をかなり短く書けます。
そのため、C++17以降のコードでは、可変長テンプレートと畳み込み式を組み合わせる書き方がよく使われます。
空のパックには注意が必要
畳み込み式では、引数が0個の場合の扱いに注意が必要です。
演算子によっては、空のパックに対して畳み込み式を使うと問題になります。
たとえば、複数の値を足す処理では、引数が1つもない場合に何を返すべきかが明確でないことがあります。
そのような場合は、初期値を用意する必要があります。
一方で、論理積や論理和など、一部の演算子では空のパックに対する既定の扱いが決められています。
完全転送と可変長テンプレート
引数を効率よく別の関数へ渡す仕組み
可変長テンプレートは、完全転送と組み合わせて使われることが多いです。
完全転送とは、受け取った引数の性質をできるだけ保ったまま、別の関数へ渡す仕組みです。
ここでいう性質とは、主に左辺値か右辺値か、コピーすべきかムーブできるか、といった情報です。
ラッパー関数やファクトリ関数で重要
完全転送は、ラッパー関数やファクトリ関数で特に重要です。
たとえば、ある関数が受け取った引数を、内部で別の関数やコンストラクタにそのまま渡す場合、余計なコピーを発生させたくありません。
完全転送を使うと、呼び出し側が渡した引数の性質をなるべく維持したまま、中継先へ渡せます。
値渡しとの違い
可変長テンプレートで単純に引数を値渡しすると、コピーが発生する可能性があります。
小さな値であれば問題にならないこともありますが、大きなオブジェクトやコピーできないオブジェクトを扱う場合には不適切なことがあります。
読み取り専用であれば参照で受け取り、別の関数へそのまま渡したい場合は完全転送を検討するのが一般的です。
std::initializer_list の特徴
同じ型の値を複数受け取りたい場合に便利
std::initializer_list は、同じ型の値を複数まとめて受け取るときに便利です。
たとえば、複数の数値、複数の文字列、複数の設定項目などを受け取る場合に向いています。
可変長テンプレートほど汎用的ではありませんが、用途が合っていればシンプルで読みやすいコードになります。
型が混在する処理には向かない
std::initializer_list は、基本的に同じ型の要素を扱うための仕組みです。
異なる型の値を自由に受け取りたい場合には向いていません。
整数、文字列、小数などを混在させて受け取りたい場合は、可変長テンプレートの方が適しています。
呼び出し側の意図が分かりやすい
std::initializer_list を使うと、呼び出し側では「値のリストを渡している」という意図が分かりやすくなります。
そのため、同じ型のデータをまとめて処理する関数では、可変長テンプレートよりも std::initializer_list の方が自然な場合があります。
C形式の可変長引数と可変長テンプレートの違い
型安全性が大きく異なる
C形式の可変長引数と可変長テンプレートの最も大きな違いは、型安全性です。
C形式の可変長引数では、可変長部分の型情報を安全に扱えません。
そのため、関数側が間違った型として値を取り出してしまうリスクがあります。
一方、可変長テンプレートでは、コンパイラが引数の型を把握します。
これにより、型に合わない処理をコンパイル時に検出しやすくなります。
引数の個数管理も異なる
C形式の可変長引数では、引数の個数を別途管理する必要があります。
その個数が実際の引数数と合っていない場合、危険な動作につながります。
可変長テンプレートでは、引数の個数をコンパイラが把握しているため、C形式より安全に扱えます。
現代C++では可変長テンプレートが基本
新しくC++のコードを書く場合は、基本的に可変長テンプレートを使うべきです。
C形式の可変長引数は、C APIとの互換性が必要な場合や、既存コードとの連携が必要な場合に限って検討するのがよいでしょう。
std::format や std::print との関係
書式付き出力には現代的な選択肢がある
C++で可変長引数というと、printf のような書式付き出力を思い浮かべる人も多いです。
しかし、現代C++では std::format や std::print のような、よりC++らしい仕組みもあります。
std::format はC++20で導入された書式付き文字列を作るための機能です。
std::print や std::println はC++23で追加された出力機能です。
printf より型安全に扱いやすい
printf では、フォーマット指定と実際の引数の型をプログラマーが正しく合わせる必要があります。
これに対して、std::format はC++の型システムとより相性がよく、printf よりも型安全に扱いやすい設計になっています。
ただし、標準ライブラリやコンパイラの対応状況によって使える機能に差があるため、実務で使う場合は開発環境の対応を確認する必要があります。
実務での使い分け
型が異なる任意個数の引数を扱いたい場合
型が異なる複数の引数を柔軟に扱いたい場合は、可変長テンプレートが適しています。
たとえば、ログ出力、メッセージ生成、関数呼び出しの中継、オブジェクト生成の補助などでよく使われます。
現代C++では、この用途でC形式の可変長引数を使う必要はほとんどありません。
同じ型の値を複数扱いたい場合
同じ型の値を複数まとめて扱いたい場合は、std::initializer_list が適しています。
たとえば、複数の数値を合計する、複数の文字列を登録する、複数の設定項目を渡すといった場面です。
この場合、可変長テンプレートよりも std::initializer_list の方がシンプルで分かりやすいことがあります。
C APIと連携する場合
C言語のライブラリや古いAPIと連携する場合には、C形式の可変長引数を扱う必要が出てくることがあります。
ただし、その場合でも型の不一致や引数数の不一致に注意する必要があります。
新しく設計するC++のAPIでは、できるだけC形式の可変長引数を避け、より安全な仕組みを使うのが望ましいです。
書式付き文字列を扱いたい場合
書式付き文字列を作りたい場合は、printf よりも std::format を検討するとよいです。
出力まで行いたい場合は、環境が対応していれば std::print や std::println も選択肢になります。
ただし、C++20やC++23の機能は、コンパイラや標準ライブラリの対応状況に左右されるため、実際に使う前に確認が必要です。
可変長テンプレートを使うときの注意点
エラーメッセージが長くなりやすい
可変長テンプレートは強力ですが、テンプレートの展開に失敗したときのエラーメッセージが長くなることがあります。
特に、引数の型が多い場合や、テンプレートが複雑に組み合わさっている場合は、エラーの原因を読み解くのが難しくなることがあります。
そのため、実務では関数の役割を明確にし、必要に応じて型制約を付けることが重要です。
何でも受け取れてしまうことがある
可変長テンプレートは、制約を付けなければ非常に多くの型を受け取れます。
これは柔軟である一方、意図しない型まで受け取ってしまう原因にもなります。
C++20以降であれば、concept を使って「この条件を満たす型だけ受け取る」といった制約を付けられます。
これにより、エラーメッセージを分かりやすくしたり、関数の使い方を明確にしたりできます。
コピーやムーブに注意する
可変長テンプレートでは、引数の受け取り方によってコピーやムーブの挙動が変わります。
単純な値渡しでは、不要なコピーが発生することがあります。
読み取り専用であれば参照で受け取る、別の関数へそのまま渡すなら完全転送を使う、というように、目的に応じて受け取り方を選ぶことが大切です。
よくある誤解
可変長引数はすべて同じ仕組みだと思ってしまう
C++には複数の可変長引数の仕組みがあります。
C形式の可変長引数、可変長テンプレート、std::initializer_list は、それぞれ目的も安全性も異なります。
特に、C形式の可変長引数と可変長テンプレートは大きく違います。
同じ「可変長」という言葉でまとめられますが、実務では別物として理解する必要があります。
C形式の可変長引数を気軽に使ってしまう
C形式の可変長引数は一見便利に見えますが、型安全性が低く、間違いに気づきにくいという問題があります。
C++で新しく関数を設計する場合は、まず可変長テンプレートや std::initializer_list を検討するべきです。
C形式の可変長引数は、C互換が必要な場合に限定して使うのが安全です。
std::initializer_list を万能な可変長引数だと思ってしまう
std::initializer_list は、任意の型を自由に受け取る仕組みではありません。
基本的には、同じ型の値をリストとして受け取るためのものです。
異なる型を混在させたい場合や、引数ごとに型を保持したい場合は、可変長テンプレートを使う必要があります。
まとめ
C++の可変長引数には、複数の方法があります。
C言語由来の可変長引数は古くからある仕組みですが、型安全性が低く、引数の個数や型をプログラマーが正しく管理しなければなりません。
そのため、新しくC++のコードを書く場合には基本的に避けた方がよいです。
現代C++で任意個数・任意型の引数を扱うなら、可変長テンプレートが基本です。
可変長テンプレートは型情報を保持でき、コンパイル時に多くの誤りを検出しやすいため、C形式の可変長引数よりも安全で柔軟です。
一方、同じ型の値を複数まとめて受け取りたいだけであれば、std::initializer_list が向いています。用途が限定される分、シンプルで読みやすいコードにしやすいです。
実務では、次のように使い分けると分かりやすいです。
型が異なる任意個数の引数を扱いたい場合は、可変長テンプレートを使います。
同じ型の値を複数まとめて受け取りたい場合は、std::initializer_list を使います。
C言語のAPIや古いライブラリと連携する必要がある場合のみ、C形式の可変長引数を検討します。
書式付き文字列を作る場合は、環境が対応していれば std::format や std::print も選択肢になります。
C++の可変長引数を理解するうえで最も重要なのは、現代C++では可変長テンプレートを中心に考えるということです。
C形式の可変長引数は仕組みとして知っておくべきですが、新規コードでは安全性の高い可変長テンプレートを優先するのが基本です。
以上、C++の可変長引数についてでした。
最後までお読みいただき、ありがとうございました。
