コンピュータプログラミングにおいて、可変引数テンプレート(またはバリエーションジェネリクス)とは、可変個の型パラメータを受け取るテンプレートのことです。これらは複数のプログラミング言語に存在し、可変個の型を持つ型安全なジェネリクスを作成するために使用されます。可変引数関数や可変引数マクロと性質がやや似ています。
可変引数テンプレートは、C++ ( C++11規格以降)とD言語でサポートされています。JavaやC#などの他の言語は型安全な可変引数関数をサポートしていますが、C++やDのような可変引数ジェネリクスはサポートしていません。
C++ に可変引数テンプレートが導入される前は、可変引数関数はCのva_list可変引数マクロを使用してのみ実装できましたが、これらは型安全性がありませんでした。C++ の可変引数テンプレート機能は、Douglas Gregor と Jaakko Järvi [ 1 ] [ 2 ]によって設計され、後に C++11 で標準化されました。C++11 より前は、テンプレート (クラスと関数) は固定数の引数しか受け取ることができず、その数はテンプレートが最初に宣言されたときに指定する必要がありました。C++11 では、テンプレート定義で任意の数の任意の型の引数を受け取ることができます。したがって、C++ には Java や C# のような非テンプレート可変引数関数がないため、型安全な可変引数関数を取得する唯一の方法は、関数に対する可変引数テンプレートを使用することです。
// 0 個以上の引数を取るtemplate < typename ... Ts > class Tuple ;上記のテンプレートクラスTuple<Ts...>(製品タイプまたはタプルを表す)は、任意の数の型名をテンプレートパラメータとして受け取ります。ここでは、上記のテンプレートクラスのインスタンスが3つの型引数でインスタンス化されています。
using String = std :: string ; template < typename T > using Vector = std :: vector <T> ; template < typename K , typename V > using TreeMap = std :: map < K , V > ;Tuple < int , Vector < int > , TreeMap < String , Vector < int >>> t ;引数の数はゼロでも構いませんので、それでも動作します。Tuple<>t;
可変引数テンプレートが正の数の引数のみを許容する場合、次の定義を使用できます。
// 1 つ以上の引数を取りますtemplate < typename First , typename ... Rest > class Tuple ;タプルの一般的な実装では、次のような再帰的な分解が使用されます。
// 前方宣言テンプレート< typename ... Ts > class Tuple ;// 空のケーステンプレート<> class Tuple <> {};// 再帰的なケースtemplate < typename Head , typename ... Tail > class Tuple < Head , Tail ... > { private : Head head ; Tuple < Tail ... > tail ; public : Tuple () = default ; explicit Tuple ( const Head & head , const Tail & ... tail ) : head { head }, tail { tail ...} {}// ... };可変引数テンプレートは関数にも適用できるため、可変引数関数( printf()など)に型安全なアドオンを提供するだけでなく、printf-like 構文で呼び出される関数が非自明なオブジェクトを処理できるようになります。
std :: string_viewを使用します。template < typename ... Args > void myPrintf ( string_view fmt , Args ... parameters );省略記号( ... ) 演算子には 2 つの役割があります。パラメーター名の左側に省略記号がある場合、パラメーター パックが宣言されます。パラメーター パックを使用すると、ユーザーは可変引数テンプレート パラメーターに 0 個以上の引数をバインドできます。パラメーター パックは、型指定のないパラメーターにも使用できます。一方、省略記号がテンプレートまたは関数呼び出しの引数の右側にある場合、パラメーター パックは、以下の例のように個別の引数に展開されます。実際には、コード内で省略記号を使用すると、省略記号の前の式全体が、引数パックから展開される後続の引数ごとに繰り返され、式はカンマで区切られます。args...printf()
固定小数点コンビネータは、可変引数テンプレート引数を使用して実装されることが多い(はと同等であることに注意してください)。[](auto&&...args)[]<template...Args>(Args&&...args)
auto fix = []( auto f ) { return [ f ]( auto && ... args ) -> decltype ( auto ) { return f ( f , std :: forward < decltype ( args ) > ( args )...); }; };auto factorial = fix ([]( auto self , long n ) -> long { return n == 0 ? 1 : n * self ( self , n - 1 ); });std :: println ( "5! = {}" , factorial ( 5 )); // 120と出力されますTJava でパラメータを型( に類似)のみに制限するには、概念[ 3 ]または(変換可能であることを意図した型の場合)T... argsを使用できます。 [ 4 ]std::same_as<T, U>std::convertible_to<From, To>
std :: same_asを使用します。// Java宣言と同等// <T> void fn(T... args) template < typename T > void fn ( same_as < T > auto ... args ) { // ... }これはさらに特定のタイプに限定することもできます。
using std :: convertible_to ; using std :: same_as ; using std :: string ;// Java宣言と同等// void foo(int... args) void foo ( same_as < int > auto ... args ) { // ... }// Java宣言と同等// void bar(String... args) void bar ( convertible_to < string > auto ... args ) { // ... }可変引数テンプレートの使用は、多くの場合再帰的です。可変引数パラメータ自体は、関数やクラスの実装から容易に利用できるわけではありません。したがって、C++11の可変引数printf()置換のようなものを定義する典型的なメカニズムは次のようになります。
std :: runtime_errorを使用します。// 基本ケースvoid myPrintf ( const char s []) { while ( * s ) { if ( * s == '%' ) { if ( * ( s + 1 ) == '%' ) { ++ s ; } else { throw runtime_error ( "無効なフォーマット文字列: 引数がありません" ); } } std :: println ( "{}" , * s ++ ); } }// 再帰テンプレート< typename T , typename ... Args > void myPrintf ( const char s [], T value , Args ... args ) { while ( * s ) { if ( * s == '%' ) { if ( * ( s + 1 ) != '%' ) { // フォーマットを解析するふりをする: 2 文字のフォーマット文字列 ( %d、%f など) でのみ機能する。 %5.4f では失敗する。s += 2 ; // 値を出力するstd :: println ( "{}" , value ); // *s が 0 の場合でも呼び出されるが、その場合は何も行わない (余分な引数は無視される) myPrintf ( s , args ...); return ; } ++ s ; } std :: println ( "{}" , * s ++ ); } }これは再帰的なテンプレートです。可変引数テンプレートのバージョンはmyPrintf()自身を呼び出すか、(が空の場合はargs...)基本ケースを呼び出すことに注意してください。
可変引数テンプレートの値を反復処理する単純なメカニズムはありません。しかし、引数パックを各パラメータごとに個別に評価できる単一の引数に変換する方法はいくつかあります。通常、これは関数オーバーロードに依存するか、関数が一度に1つの引数を選択できる場合は、単純な展開マーカーを使用します。
template < typename ... Args > inline void pass ( Args && ... args ) { // ... }これは以下のように使用できます。
template < typename ... Args > inline void expand ( Args && ... args ) { pass ( foo ( args )...); }expand ( 42 , "answer" , true );これは以下のように展開されます。
pass ( foo ( arg1 ), foo ( arg2 ), foo ( arg3 ) /* など... */ );この「pass」関数の使用は必須です。引数パックの展開は、関数呼び出し引数をカンマで区切って行われますが、カンマはカンマ演算子と等価ではないためです。したがって、は決して機能しません。さらに、上記の解決策は、の戻り値の型がでない場合にのみ機能します。また、関数引数の評価順序が未定義であるため、呼び出しは未指定の順序で実行されます。未指定の順序を回避するには、厳密な左から右への評価順序を保証する中括弧で囲まれた初期化リストを使用できます。初期化リストには非の戻り値の型が必要ですが、カンマ演算子を使用して各展開要素に対してを生成できます。foo(args)...;foo()voidfoo()void1
struct Pass { template < typename ... T > explicit Pass ( T ... args ) { // ... } };パス{( foo ( args ), 1 )...};関数を実行する代わりに、ラムダ式を指定してその場で実行することで、任意のステートメントのシーケンスをその場で実行できます。
Pass {([ & ]() -> void { std :: println ( "{}" , args ); }(), 1 )...};しかし、この例ではラムダ関数は必ずしも必要ではありません。代わりに、より一般的な式を使用できます。
渡す{( std :: println ( "{}" , args ), 1 )...};C++17では、これらはカンマ演算子のfold式を使って書き換えることができます。
([ & ]() -> void { std :: println ( "{}" , args ); }(), ...); (( std :: println ( "{}" , args )), ...);もう一つの方法は、関数の「終了バージョン」を使ったオーバーロードです。これはより汎用的ですが、作成には少し多くのコードと労力が必要です。一方の関数は特定の型の引数を1つと引数パックを受け取りますが、もう一方の関数はどちらも受け取りません。(両方の関数が同じ初期パラメータリストを持っている場合、可変引数パックだけでは呼び出しの曖昧さを解消できないため、呼び出しは曖昧になります。)例:
void foo () { // 終了バージョン}template < typename First , typename ... Args > void foo ( const First & first , const Args && ... args ) { process ( first ); foo ( args ...); // 注: first はここには現れません! }引数が少なくとも 1 つ含まれている場合はargs...、2 番目のバージョンにリダイレクトされます。パラメータ パックが空の場合は、単に終了バージョンにリダイレクトされ、何も実行されません。
可変引数テンプレートは、例外指定、基底クラスリスト、またはコンストラクタの初期化リストでも使用できます。たとえば、クラスは次のように指定できます。
template < typename ... Bases > class Foo : public Bases ... { public : explicit Foo ( Bases && ... bases ) : Bases ( bases )... {} };アンパック演算子は、の基底クラスの型を複製するFooため、このクラスは渡された各型から派生します。また、コンストラクタは、の基底クラスを初期化するために、各基底クラスへの参照を受け取る必要がありますFoo。
関数テンプレートに関しては、可変引数パラメータを転送できます。ユニバーサル参照(上記参照)と組み合わせると、次のスマートポインタの例のように、完全な転送が可能になります。
template < typename T > using SharedPtr = std :: shared_ptr < T > ;template < typename T > struct SharedPtrAllocator { template < typename ... Args > SharedPtr < T > construct ( Args && ... params ) { return SharedPtr < T > ( new T ( std :: forward < Args > ( params )...)); } };これは引数リストを のコンストラクタに展開しますT。構文は、右辺値性に関しても、引数を適切な型としてコンストラクタに完全に転送します。展開演算子は、転送構文を各パラメータに伝播します。この特定のファクトリ関数は、メモリリークに関してある程度の安全性を確保するために、割り当てられたメモリを自動的に でラップします。std::forward<Args...>(params)std::shared_ptr<T>
さらに、テンプレートパラメータパック内の引数の数は、次のように決定できます。
template < typename ... Args > struct SizeCarrier { static constexpr size_t SIZE = sizeof ...( Args ); };この式はを生成し、 はを生成します。SizeCarrier<T,U>::SIZE2SizeCarrier<>::SIZE0
C++26では、可変引数テンプレートに依存する可変引数フレンドが言語に追加されました。
import std ;template < typename ... Friends > class WithFriends { private : int secretValue ;friend Friends ...; public : explicit WithFriends ( int secret ) : secretValue { secret } {} };class A { public : void readSecret ( const class WithFriends < A , B >& instance ) const { std :: println ( "A から秘密の値を調べています: {}" , instance . secretValue ); } };class B { public : void readSecret ( const class WithFriends < A , B >& instance ) const { std :: println ( "B から秘密の値を調べています: {}" , instance . secretValue ); } };int main ( int argc , char * argv [ ] ) { WithFriends < A , B > secretHolder ( 135 ) ; A a ; B b ; a.readSecret ( secretHolder ); b.readSecret ( secretHolder ) ; }パックインデックスは C++26 で C++ に導入され、再帰の少ない構文でパックの指定されたインデックスにある型または値を取得します。ただし、インデックスは定数式である必要があります。[ 5 ]パックインデックスの構文は次のとおりです。id-expression...[expression]
import std ;using std :: index_sequence ; using std :: index_sequence_for ; using std :: tuple ;// この関数 reversed() は任意の数の引数を受け取り、// 同じ引数を逆順で含み、// Ts... の各 T が decay_t<T> として減衰されたタプル <Ts...> を返します。template < typename ... Ts > requires ( sizeof ...( Ts ) > 0 ) constexpr auto reversed ( Ts && ... args ) noexcept { constexpr size_t MAX_INDEX_SIZE = sizeof ...( args ) - 1 ; auto reverser = [ & args ...] < size_t ... Is > ( index_sequence < Is ... > ) { auto reverseForOneIndex = [ & args ...] < size_t I > { return args ...[ MAX_INDEX_SIZE - I ]; }; return std :: make_tuple ( reverseForOneIndex . template operator () < Is > ()...); }; return reverser ( index_sequence_for < Ts ... > {}); }int main () { // 結果の型: // tuple<char, long double, double, float, const char*, int> constexpr tuple result = reversed ( 13 , " hello" , 2.718f , 3.14 , 9.9375L , 'X' ); static_assert ( std :: get <0> ( result ) == 'X' ) ; }C++26 では、 で宣言された展開文 (ループ)が追加され、を反復処理したり、やのようなコレクション型、および型/パラメータパック ( ) を使用したりできます。[ 6 ]templatefortemplate for (init_stmtopt; for-range-declaration : expansion-initializer) compound-stmtstd::tuple<Ts...>std::array<T, N>std::vector<T>Ts...args
import std ;void printAllObjects ( auto && ... args ) { template for ( auto && arg : { args ...}) { std :: print ( "{}" , arg ); } }int main () { printAllObjects ( "13 + 31 = " , 44 , " \nHello , world! \n " , 3.14 , '\n' , 2.718f , '\n' , 735.9572L , false ); return 0 ; }展開文はフロー文を可能にしbreak、continue制御します。フロー文は他のループと同様に動作します。[ 6 ]
import std ;using std :: index_sequence ; using std :: index_sequence_for ;template < typename ... Ts > constexpr bool areEvenIndicesEven ( Ts && ... args ) noexcept { auto checkEvenIndices = [ & args ...] < size_t ... Is > ([[ maybe_unused ]] index_sequence < Is ... > s ) -> bool { template for ( constexpr size_t I : { Is ...}) { if constexpr ( I % 2 != 0 ) { continue ; } if ( args ...[ I ] % 2 != 0 ) { return false ; } } return true ; }; return checkEvenIndices ( index_sequence_for < Ts ... > {}); }int main () { static_assert ( areEvenIndicesEven ( 0 , 1 , 2 , 3 )); }Carbon はC++ との相互運用性を念頭に置いて設計された言語であり、可変引数、特にパック展開を提供します。[ 7 ]
// 任意の要素型を持つ任意の数のベクトルを受け取り、// ベクトルの i 番目の要素が入力ベクトルの i 番目の要素のタプルであるタプルのベクトルを返します。fn Zip [ ... each ElementType :! type ]( ... each vector : Vector ( each ElementType )) -> Vector (( ... each ElementType )) { ... var each iter : auto = each vector . Begin (); var result : Vector (( ... each ElementType )); while ( ... and each iter != each vector . End ()) { result . push_back (( ... each iter )); ... each iter ++ ; } return result ; }D言語における可変引数テンプレートの定義は、C++におけるそれと類似している。
template VariadicTemplate ( Args ...) { // 本体はこちら... }同様に、引数リストの前に任意の引数を指定できます。
template VariadicTemplate ( T , string value , alias symbol , Args ...) { // 本体はここに記述... }可変引数は、その使用方法において定数配列と非常によく似ています。反復処理が可能で、インデックスによるアクセス、lengthプロパティの保持、スライスも可能です。演算はコンパイル時に解釈されるため、オペランドは実行時値(関数パラメータなど)であってはなりません。
コンパイル時に既知のものはすべて可変引数として渡すことができます。可変引数はテンプレートエイリアス引数charに似ていますが、基本型( 、、 …)も受け入れるため、より強力shortですint。
以下は、可変引数の文字列表現を出力する例です。StringOfそして、StringOf2同じ結果を生成します。
static int staticInt ;struct Dummy {}void main () { pragma ( msg , StringOf !( "Hello world" , uint , Dummy , 42 , staticInt )); pragma ( msg , StringOf2 !( "Hello world" , uint , Dummy , 42 , staticInt )); }template StringOf ( Args ...) { enum StringOf = Args [ 0 ]. stringof ~ StringOf !( Args [ 1. .$]); }template StringOf () { enum StringOf = "" ; }template StringOf2 ( Args ...) { static if ( Args . length == 0 ) { enum StringOf2 = "" ; } else { enum StringOf2 = Args [ 0 ]. stringof ~ StringOf2 !( Args [ 1. .$]); } }出力:
「こんにちは世界」uintDummy42staticInt 「こんにちは世界」uintDummy42staticInt 可変引数テンプレートは、 std.meta.AliasSeqという名前のエイリアスのシーケンスを作成するためによく使用されます。その定義は実際には非常に単純です。std.meta.AliasSeq
エイリアスAliasSeq ( Args ...) = Args ;この構造により、自動展開される可変引数リストを操作できます。引数は、コンパイル時に既知のシンボルまたは値である必要があります。これには、値、型、関数、さらには特殊化されていないテンプレートも含まれます。これにより、想定されるあらゆる操作が可能になります。
import std.meta ;void main () { // 注: AliasSeq は変更できず、エイリアスは再バインドできないため、変更には新しい名前を定義する必要があります。alias numbers = AliasSeq !( 1 , 2 , 3 , 4 , 5 , 6 ); // スライスalias lastHalf = numbers [$ / 2 .. $]; static assert ( lastHalf == AliasSeq !( 4 , 5 , 6 )); // AliasSeq の自動展開alias digits = AliasSeq !( 0 , numbers , 7 , 8 , 9 ); static assert ( digits == AliasSeq !( 0 , 1 , 2 , 3 , 4 , 5 , 6 , 7 , 8 , 9 ) ); // std.meta は、anySatisfy、allSatisfy、staticMap、Filter など、AliasSeq を操作するためのテンプレートを提供します。alias evenNumbers = Filter !( isEven , digits ); static assert ( evenNumbers == AliasSeq !( 0 , 2 , 4 , 6 , 8 )); }template isEven ( int number ) { enum isEven = ( 0 == ( number % 2 )); }C# / .NETでは、このクラスにSystem.Tupleは 1 ~ 8 個の要素に対応する特定のインスタンス化があります。9 個以上のコンポーネントを持つタプルを作成するには、C# には可変引数テンプレート パラメーターがないため、最後のパラメーターTRestはTuple<T1, T2, T3, T4, T5, T6, T7, TRest>別のタプルとして指定されます。[ 8 ]
Javaでは、トップ型を使用して可変引数をエミュレートできますjava.lang.Object(C#/.NETでも同様ですSystem.Object)が、これはC++スタイルの可変引数の型安全性を欠いています。
import java.util.List ;void printAllObjects ( Object ... args ) { for ( Object arg : args ) { System.out.println ( arg ) ; } }void main () { printAllObjects ( " Hello , world!" , 42 , 3.14159f , true , List.of ( 1 , 2 , 3 )); }Rustでは2013 年以来、可変引数ジェネリクスの提案が存在していますが[ 9 ]、今のところ言語には追加されていません。しかし、これらは可変引数マクロを使用して同様にエミュレートすることができ、コード生成に使用できます。[ 10 ]
テンプレート以外の可変引数構造に関する記事については、